refactor: CB-1093execution contexts

This commit is contained in:
Wroud
2021-07-16 22:44:16 +03:00
parent 799824d581
commit 96d45facf8
40 changed files with 1479 additions and 1225 deletions
@@ -50,6 +50,8 @@ import {
} from '@cloudbeaver/core-authentication';
import { BlocksLocaleService } from '@cloudbeaver/core-blocks';
import {
ConnectionExecutionContextResource,
ConnectionExecutionContextService,
ConnectionsManagerService,
ConnectionInfoResource,
ContainerResource,
@@ -156,6 +158,8 @@ export const coreManifest: PluginManifest = {
ContainerResource,
DBDriverResource,
NetworkHandlerResource,
ConnectionExecutionContextResource,
ConnectionExecutionContextService,
ConnectionsManagerService,
ScreenService,
AppScreenService,
@@ -0,0 +1,77 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 { makeObservable, observable } from 'mobx';
import type { ITask, TaskScheduler } from '@cloudbeaver/core-executor';
import type { ConnectionExecutionContextResource } from './ConnectionExecutionContextResource';
import type { IConnectionExecutionContext } from './IConnectionExecutionContext';
import type { IConnectionExecutionContextInfo } from './IConnectionExecutionContextInfo';
export class ConnectionExecutionContext implements IConnectionExecutionContext {
get context(): IConnectionExecutionContextInfo | undefined {
return this.connectionExecutionContextResource.get(this.contextId);
}
get executing(): boolean {
return this.scheduler.isExecuting(this.contextId);
}
get cancellable(): boolean {
return this.currentTask?.cancellable || false;
}
private currentTask: ITask<any> | null;
constructor(
private scheduler: TaskScheduler<string>,
private connectionExecutionContextResource: ConnectionExecutionContextResource,
private contextId: string
) {
this.currentTask = null;
makeObservable<this, 'currentTask'>(this, {
currentTask: observable.ref,
});
}
run<T>(task: () => Promise<T>, cancel?: () => Promise<any> | void): ITask<T> {
if (!this.context) {
throw new Error('Execution Context not found');
}
this.currentTask = this.scheduler
.schedule(this.contextId, task, { cancel })
.finally(() => {
this.currentTask = null;
});
return this.currentTask;
}
async cancel(): Promise<void> {
await this.scheduler.cancel(this.contextId);
}
async destroy(): Promise<void> {
if (!this.context) {
return;
}
await this.cancel();
await this.connectionExecutionContextResource.destroy(this.contextId);
}
async update(defaultCatalog?: string, defaultSchema?: string): Promise<void> {
if (!this.context) {
throw new Error('Execution Context not found');
}
await this.connectionExecutionContextResource.update(this.contextId, defaultCatalog, defaultSchema);
}
}
@@ -0,0 +1,199 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 { injectable } from '@cloudbeaver/core-di';
import { SessionDataResource } from '@cloudbeaver/core-root';
import {
GraphQLService,
CachedMapResource,
resourceKeyList,
ResourceKey,
ResourceKeyUtils,
ResourceKeyList,
} from '@cloudbeaver/core-sdk';
import { flat, MetadataMap } from '@cloudbeaver/core-utils';
import { ConnectionInfoResource } from '../ConnectionInfoResource';
import type { IConnectionExecutionContextInfo } from './IConnectionExecutionContextInfo';
@injectable()
export class ConnectionExecutionContextResource extends CachedMapResource<string, IConnectionExecutionContextInfo> {
static keyAll = resourceKeyList(['all'], 'all');
private loadedKeyMetadata: MetadataMap<string, boolean>;
constructor(
private graphQLService: GraphQLService,
private connectionInfoResource: ConnectionInfoResource,
sessionDataResource: SessionDataResource
) {
super();
this.loadedKeyMetadata = new MetadataMap(() => false);
sessionDataResource.onDataOutdated.addHandler(() => this.markOutdated());
sessionDataResource.onDataUpdate.addHandler(async () => {
await this.load(ConnectionExecutionContextResource.keyAll);
});
connectionInfoResource.onItemAdd.addHandler(this.updateConnectionContexts.bind(this));
connectionInfoResource.onItemDelete.addHandler(this.deleteConnectionContexts.bind(this));
this.addAlias(ConnectionExecutionContextResource.keyAll, key => {
if (this.keys.length > 0) {
return resourceKeyList(this.keys, ConnectionExecutionContextResource.keyAll.mark);
}
return ConnectionExecutionContextResource.keyAll;
});
}
has(id: string): boolean {
if (this.loadedKeyMetadata.has(id)) {
return this.loadedKeyMetadata.get(id);
}
return this.data.has(id);
}
updateConnectionContexts(key: ResourceKey<string>): void {
this.delete(
resourceKeyList(
flat(ResourceKeyUtils.map(
key,
connectionId => this.values.filter(context => {
const connection = this.connectionInfoResource.get(connectionId);
return context.connectionId === connectionId && !connection?.connected;
})
)).map(context => context.id)
)
);
}
deleteConnectionContexts(key: ResourceKey<string>): void {
this.delete(
resourceKeyList(
flat(ResourceKeyUtils.map(
key,
connectionId => this.values.filter(context => context.connectionId === connectionId)
)).map(context => context.id)
)
);
}
async create(
connectionId: string,
defaultCatalog?: string,
defaultSchema?: string
): Promise<IConnectionExecutionContextInfo> {
return await this.performUpdate('', [], async () => {
const { context } = await this.graphQLService.sdk.executionContextCreate({
connectionId,
defaultCatalog,
defaultSchema,
});
this.updateContexts(context);
return this.get(context.id)!;
});
}
async update(
contextId: string,
defaultCatalog?: string,
defaultSchema?: string
): Promise<IConnectionExecutionContextInfo> {
const context = this.get(contextId);
if (!context) {
throw new Error('Execution context not found');
}
await this.performUpdate(contextId, [], async () => {
await this.graphQLService.sdk.executionContextUpdate({
contextId,
connectionId: context.connectionId,
defaultCatalog,
defaultSchema,
});
context.defaultCatalog = defaultCatalog;
context.defaultSchema = defaultSchema;
});
return context;
}
async destroy(contextId: string): Promise<void> {
const context = this.get(contextId);
if (!context) {
return;
}
await this.performUpdate(contextId, [], async () => {
await this.graphQLService.sdk.executionContextDestroy({
contextId,
connectionId: context.connectionId,
});
});
this.delete(contextId);
}
async loadAll(): Promise<IConnectionExecutionContextInfo[]> {
this.resetIncludes();
await this.load(ConnectionExecutionContextResource.keyAll);
return this.values;
}
async refreshAll(): Promise<IConnectionExecutionContextInfo[]> {
this.resetIncludes();
await this.refresh(ConnectionExecutionContextResource.keyAll);
return this.values;
}
refreshAllLazy(): void {
this.resetIncludes();
this.markOutdated(ConnectionExecutionContextResource.keyAll);
this.loadedKeyMetadata.set(ConnectionExecutionContextResource.keyAll.list[0], false);
}
protected async loader(
key: ResourceKey<string>
): Promise<Map<string, IConnectionExecutionContextInfo>> {
const all = ResourceKeyUtils.hasMark(key, ConnectionExecutionContextResource.keyAll.mark);
await ResourceKeyUtils.forEachAsync(key, async contextId => {
const { contexts } = await this.graphQLService.sdk.executionContextList({
contextId: all ? undefined : contextId,
// connectionId
});
const key = this.updateContexts(...contexts);
for (const contextId of this.keys) {
if (!ResourceKeyUtils.includes(key, contextId)) {
this.delete(contextId);
}
}
});
if (all) {
this.loadedKeyMetadata.set(ConnectionExecutionContextResource.keyAll.list[0], true);
}
return this.data;
}
private updateContexts(...contexts: IConnectionExecutionContextInfo[]): ResourceKeyList<string> {
const key = resourceKeyList(contexts.map(context => context.id));
const oldContexts = this.get(key);
this.set(key, oldContexts.map((context, i) => ({ ...context, ...contexts[i] })));
return key;
}
}
@@ -0,0 +1,52 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 { injectable } from '@cloudbeaver/core-di';
import { TaskScheduler } from '@cloudbeaver/core-executor';
import { MetadataMap } from '@cloudbeaver/core-utils';
import { ResourceKeyUtils } from '../../../core-sdk/src';
import { ConnectionExecutionContext } from './ConnectionExecutionContext';
import { ConnectionExecutionContextResource } from './ConnectionExecutionContextResource';
@injectable()
export class ConnectionExecutionContextService {
private contexts: MetadataMap<string, ConnectionExecutionContext>;
protected scheduler: TaskScheduler<string>;
constructor(
private connectionExecutionContextResource: ConnectionExecutionContextResource
) {
this.contexts = new MetadataMap(contextId => new ConnectionExecutionContext(
this.scheduler,
this.connectionExecutionContextResource,
contextId
));
this.scheduler = new TaskScheduler((a, b) => a === b);
this.connectionExecutionContextResource.onItemDelete.addHandler(
key => ResourceKeyUtils.forEach(key, contextId => this.contexts.delete(contextId))
);
}
get(contextId: string): ConnectionExecutionContext | undefined {
if (!this.connectionExecutionContextResource.has(contextId)) {
return undefined;
}
return this.contexts.get(contextId);
}
async create(
connectionId: string,
defaultCatalog?: string,
defaultSchema?: string
): Promise<ConnectionExecutionContext> {
const context = await this.connectionExecutionContextResource.create(connectionId, defaultCatalog, defaultSchema);
return this.contexts.get(context.id);
}
}
@@ -0,0 +1,22 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 { ITask } from '@cloudbeaver/core-executor';
import type { IConnectionExecutionContextInfo } from './IConnectionExecutionContextInfo';
export interface IConnectionExecutionContext {
readonly context: IConnectionExecutionContextInfo | undefined;
readonly executing: boolean;
readonly cancellable: boolean;
run: <T>(task: () => Promise<T>, cancel?: () => Promise<any> | void) => ITask<T>;
cancel: () => Promise<void>;
destroy: () => Promise<void>;
update: (defaultCatalog?: string, defaultSchema?: string) => Promise<void>;
}
@@ -6,9 +6,6 @@
* you may not use this file except in compliance with the License.
*/
export interface IDatabaseExecutionContext {
connectionId: string;
contextId: string;
objectCatalogId?: string;
objectSchemaId?: string;
}
import type { SqlContextInfo } from '@cloudbeaver/core-sdk';
export interface IConnectionExecutionContextInfo extends SqlContextInfo { }
+13 -8
View File
@@ -1,3 +1,16 @@
export * from './Administration/Connections/ConnectionsAdministration';
export * from './Administration/Connections/ConnectionsAdministrationNavService';
export * from './Administration/Connections/ConnectionsAdministrationService';
export * from './Administration/Connections/CreateConnection/Manual/ConnectionManualService';
export * from './Administration/Connections/CreateConnection/Search/ConnectionSearchService';
export * from './Administration/Connections/CreateConnection/CreateConnectionBaseBootstrap';
export * from './Administration/Connections/CreateConnectionService';
export * from './Administration/ConnectionsResource';
export * from './ConnectionExecutionContext/ConnectionExecutionContext';
export * from './ConnectionExecutionContext/ConnectionExecutionContextResource';
export * from './ConnectionExecutionContext/ConnectionExecutionContextService';
export * from './ConnectionExecutionContext/IConnectionExecutionContext';
export * from './ConnectionExecutionContext/IConnectionExecutionContextInfo';
export * from './ConnectionForm/Options/ConnectionOptionsTabService';
export * from './ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService';
export * from './ConnectionForm/SSH/ConnectionSSHTabService';
@@ -12,14 +25,6 @@ export * from './ConnectionForm/ConnectionFormState';
export * from './ConnectionForm/connectionFormStateContext';
export * from './ConnectionForm/IConnectionFormProps';
export * from './ConnectionForm/useConnectionFormState';
export * from './Administration/Connections/ConnectionsAdministration';
export * from './Administration/Connections/ConnectionsAdministrationNavService';
export * from './Administration/Connections/ConnectionsAdministrationService';
export * from './Administration/Connections/CreateConnection/Manual/ConnectionManualService';
export * from './Administration/Connections/CreateConnection/Search/ConnectionSearchService';
export * from './Administration/Connections/CreateConnection/CreateConnectionBaseBootstrap';
export * from './Administration/Connections/CreateConnectionService';
export * from './Administration/ConnectionsResource';
export * from './extensions/IConnectionProvider';
export * from './extensions/IConnectionSetter';
@@ -0,0 +1,16 @@
mutation executionContextCreate(
$connectionId: ID!
$defaultCatalog: String
$defaultSchema: String
) {
context: sqlContextCreate(
connectionId: $connectionId
defaultCatalog: $defaultCatalog
defaultSchema: $defaultSchema
) {
id
connectionId
defaultCatalog
defaultSchema
}
}
@@ -0,0 +1,9 @@
mutation executionContextDestroy(
$connectionId: ID!
$contextId: ID!
) {
sqlContextDestroy(
connectionId: $connectionId
contextId: $contextId
)
}
@@ -0,0 +1,14 @@
query executionContextList(
$connectionId: ID
$contextId: ID
) {
contexts: sqlListContexts(
connectionId: $connectionId
contextId: $contextId
) {
id
connectionId
defaultCatalog
defaultSchema
}
}
@@ -0,0 +1,13 @@
mutation executionContextUpdate(
$connectionId: ID!
$contextId: ID!
$defaultCatalog: ID
$defaultSchema: ID
) {
context: sqlContextSetDefaults(
connectionId: $connectionId
contextId: $contextId
defaultCatalog: $defaultCatalog
defaultSchema: $defaultSchema
)
}
@@ -1,15 +0,0 @@
mutation sqlContextCreate(
$connectionId: ID!
$defaultCatalog: String
$defaultSchema: String
) {
context: sqlContextCreate(
connectionId: $connectionId
defaultCatalog: $defaultCatalog
defaultSchema: $defaultSchema
) {
id
defaultCatalog
defaultSchema
}
}
@@ -1,9 +0,0 @@
mutation sqlContextDestroy(
$connectionId: ID!
$contextId: ID!
) {
sqlContextDestroy(
connectionId: $connectionId
contextId: $contextId
)
}
@@ -1,13 +0,0 @@
mutation sqlContextSetDefaults(
$connectionId: ID!
$contextId: ID!
$defaultCatalog: ID
$defaultSchema: ID
) {
context: sqlContextSetDefaults(
connectionId: $connectionId
contextId: $contextId
defaultCatalog: $defaultCatalog
defaultSchema: $defaultSchema
)
}
File diff suppressed because it is too large Load Diff
@@ -192,5 +192,9 @@
text-decoration: underline;
color: white !important;
}
.cm-s-material .active-query {
background-color: $active-query;
}
}
}
@@ -31,6 +31,7 @@ $link: #219;
$error: #f00;
$delimiter: rgb(238, 204, 100);
$cursor: #fff;
$active-query: $mdc-theme-secondary;
:global .CodeMirror-hints {
@include mdc-elevation(3);
@@ -31,6 +31,7 @@ $link: #905;
$error: #f00;
$delimiter: rgb(238, 204, 100);
$cursor: #000;
$active-query: $mdc-theme-secondary;
:global .CodeMirror-hints {
@include mdc-elevation(3);
@@ -81,7 +81,7 @@ export class DataExportMenuService {
this.commonDialogService.open(DataExportDialog, {
connectionId: source.options.connectionId,
contextId: context.data.model.source.executionContext?.contextId,
contextId: context.data.model.source.executionContext?.context?.id,
containerNodePath: source.options.containerNodePath,
resultId: result.id,
sourceName: source.options.query,
@@ -6,16 +6,14 @@
* you may not use this file except in compliance with the License.
*/
import { observable, makeObservable } from 'mobx';
import type { ConnectionExecutionContextService, IConnectionExecutionContext } from '@cloudbeaver/core-connections';
import type { NotificationService } from '@cloudbeaver/core-events';
import type { GraphQLService } from '@cloudbeaver/core-sdk';
import { EDeferredState } from '@cloudbeaver/core-utils';
import type { ITask } from '@cloudbeaver/core-executor';
import type { GraphQLService, SqlExecuteInfo } from '@cloudbeaver/core-sdk';
import { DatabaseDataEditor } from './DatabaseDataModel/DatabaseDataEditor';
import { DatabaseDataSource } from './DatabaseDataModel/DatabaseDataSource';
import type { IDatabaseDataOptions } from './DatabaseDataModel/IDatabaseDataOptions';
import type { IDatabaseExecutionContext } from './DatabaseDataModel/IDatabaseExecutionContext';
import type { IDatabaseResultSet } from './DatabaseDataModel/IDatabaseResultSet';
import { FetchTableDataAsyncProcess } from './FetchTableDataAsyncProcess';
@@ -24,23 +22,20 @@ export interface IDataContainerOptions extends IDatabaseDataOptions {
}
export class ContainerDataSource extends DatabaseDataSource<IDataContainerOptions, IDatabaseResultSet> {
currentFetchTableProcess: FetchTableDataAsyncProcess | null;
currentTask: ITask<SqlExecuteInfo> | null;
get canCancel(): boolean {
return this.currentFetchTableProcess?.getState() === EDeferredState.PENDING;
return this.currentTask?.cancellable || false;
}
constructor(
private graphQLService: GraphQLService,
private notificationService: NotificationService,
private connectionExecutionContextService: ConnectionExecutionContextService
) {
super();
makeObservable(this, {
currentFetchTableProcess: observable,
});
this.currentFetchTableProcess = null;
this.currentTask = null;
this.executionContext = null;
this.editor = new DatabaseDataEditor();
}
@@ -49,17 +44,18 @@ export class ContainerDataSource extends DatabaseDataSource<IDataContainerOption
return !this.getResult(resultIndex)?.data && this.error === null;
}
cancel(): boolean {
if (this.currentFetchTableProcess) {
return this.currentFetchTableProcess.cancel();
async cancel(): Promise<void> {
if (this.currentTask) {
await this.currentTask.cancel();
}
return false;
}
async request(
prevResults: IDatabaseResultSet[]
): Promise<IDatabaseResultSet[]> {
if (!this.options?.containerNodePath) {
const options = this.options;
if (!options) {
throw new Error('containerNodePath must be provided for table');
}
@@ -69,25 +65,27 @@ export class ContainerDataSource extends DatabaseDataSource<IDataContainerOption
const fetchTableProcess = new FetchTableDataAsyncProcess(this.graphQLService, this.notificationService);
fetchTableProcess.start(
{
connectionId: executionContext.connectionId,
contextId: executionContext.contextId,
containerNodePath: this.options.containerNodePath,
},
{
offset,
limit,
constraints: this.options.constraints,
where: this.options.whereFilter || undefined,
},
this.dataFormat,
);
this.currentTask = executionContext.run(async () => {
await fetchTableProcess.start(
{
connectionId: executionContext.context!.connectionId,
contextId: executionContext.context!.id,
containerNodePath: options.containerNodePath,
},
{
offset,
limit,
constraints: options.constraints,
where: options.whereFilter || undefined,
},
this.dataFormat,
);
this.currentFetchTableProcess = fetchTableProcess;
return fetchTableProcess.promise;
}, () => { fetchTableProcess.cancel(); });
try {
const response = await fetchTableProcess.promise;
const response = await this.currentTask;
this.requestInfo = {
requestDuration: response?.duration || 0,
@@ -129,8 +127,8 @@ export class ContainerDataSource extends DatabaseDataSource<IDataContainerOption
try {
for (const update of changes) {
const response = await this.graphQLService.sdk.updateResultsDataBatch({
connectionId: executionContext.connectionId,
contextId: executionContext.contextId,
connectionId: executionContext.context!.connectionId,
contextId: executionContext.context!.id,
resultsId: update.resultId,
updatedRows: Array.from(update.diff.values()).map(diff => ({
data: diff.source,
@@ -173,39 +171,16 @@ export class ContainerDataSource extends DatabaseDataSource<IDataContainerOption
}
async dispose(): Promise<void> {
if (this.executionContext) {
await this.graphQLService.sdk.sqlContextDestroy({
connectionId: this.executionContext.connectionId,
contextId: this.executionContext.contextId,
});
}
await this.executionContext?.destroy();
}
private async ensureContextCreated(): Promise<IDatabaseExecutionContext> {
if (!this.executionContext) {
private async ensureContextCreated(): Promise<IConnectionExecutionContext> {
if (!this.executionContext?.context) {
if (!this.options) {
throw new Error('Options must be provided');
}
this.executionContext = await this.createExecutionContext(this.options.connectionId);
this.executionContext = await this.connectionExecutionContextService.create(this.options.connectionId);
}
return this.executionContext;
}
private async createExecutionContext(
connectionId: string,
defaultCatalog?: string,
defaultSchema?: string
): Promise<IDatabaseExecutionContext> {
const response = await this.graphQLService.sdk.sqlContextCreate({
connectionId,
defaultCatalog,
defaultSchema,
});
return {
contextId: response.context.id,
connectionId,
objectCatalogId: response.context.defaultCatalog,
objectSchemaId: response.context.defaultSchema,
};
}
}
@@ -6,18 +6,11 @@
* you may not use this file except in compliance with the License.
*/
import {
NavNodeManagerService,
INodeNavigationData,
ITab,
NodeManagerUtils
} from '@cloudbeaver/core-app';
import { NavNodeManagerService, INodeNavigationData, ITab } from '@cloudbeaver/core-app';
import { injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
import {
DBObjectPageService, ObjectPage, ObjectViewerTabService, IObjectViewerTabState
} from '@cloudbeaver/plugin-object-viewer';
import { DBObjectPageService, ObjectPage, ObjectViewerTabService, IObjectViewerTabState } from '@cloudbeaver/plugin-object-viewer';
import { DataPresentationService } from './DataPresentationService';
import { DataViewerPanel } from './DataViewerPage/DataViewerPanel';
@@ -35,7 +28,7 @@ export class DataViewerTabService {
private objectViewerTabService: ObjectViewerTabService,
private dbObjectPageService: DBObjectPageService,
private notificationService: NotificationService,
private dataPresentationService: DataPresentationService
private dataPresentationService: DataPresentationService,
) {
this.page = this.dbObjectPageService.register({
key: 'data_viewer_data',
@@ -70,51 +63,48 @@ export class DataViewerTabService {
if (tabInfo.isNewlyCreated) {
trySwitchPage(this.page);
}
// if (nodeInfo.childrenId === '') {
// tabInfo.trySwitchHandler(this.tabHandler);
// }
} catch (exception) {
this.notificationService.logException(exception, 'Data Viewer Error', 'Error in Data Viewer while processing action with database node');
}
}
private async handleTabSelect(tab: ITab<IObjectViewerTabState>) {
if (tab.handlerState.pageId !== this.page.key) {
return;
}
if (!tab.handlerState.connectionId) {
return;
}
const node = await this.navNodeManagerService.loadNode({
nodeId: tab.handlerState.objectId,
parentId: tab.handlerState.parentId,
});
if (tab.handlerState.pageId !== this.page.key) {
return;
}
if (!this.navNodeManagerService.isNodeHasData(node)) {
return;
}
if (this.dataViewerTableService.has(tab.id)) {
return;
}
const nodeInfo = this.navNodeManagerService
.getNodeContainerInfo(tab.handlerState.objectId);
if (!nodeInfo.connectionId) {
return;
}
let model = this.dataViewerTableService.get(tab.handlerState.tableId || '');
if (tab.handlerState.tableId && model && !model.source.executionContext?.context) {
await this.dataViewerTableService.removeTableModel(tab.handlerState.tableId);
model = undefined;
}
if (!model) {
model = await this.dataViewerTableService.create(
NodeManagerUtils.connectionNodeIdToConnectionId(nodeInfo.connectionId),
tab.handlerState.connectionId,
tab.handlerState.objectId
);
tab.handlerState.tableId = model.id;
const pageState = this.page.getState(tab);
if (pageState) {
const presentation = this.dataPresentationService.get(pageState?.presentationId);
if (presentation?.dataFormat !== undefined) {
model.setDataFormat(presentation.dataFormat);
}
@@ -123,7 +113,7 @@ export class DataViewerTabService {
// TODO: used for initial data fetch, but can repeat request each time data tab is selected,
// so probably should be refactored and managed by presentation
if (model.source.results.length === 0) {
if (model.source.error === null && model.source.results.length === 0) {
model.requestData();
}
}
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { ConnectionInfoResource } from '@cloudbeaver/core-connections';
import { ConnectionInfoResource, ConnectionExecutionContextService } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { GraphQLService } from '@cloudbeaver/core-sdk';
@@ -23,7 +23,8 @@ export class DataViewerTableService {
private tableViewerStorageService: TableViewerStorageService,
private connectionInfoResource: ConnectionInfoResource,
private graphQLService: GraphQLService,
private notificationService: NotificationService
private notificationService: NotificationService,
private connectionExecutionContextService: ConnectionExecutionContextService,
) { }
has(tableId: string): boolean {
@@ -47,7 +48,11 @@ export class DataViewerTableService {
containerNodePath = ''
): Promise<DataModelWrapper> {
const connectionInfo = await this.connectionInfoResource.load(connectionId);
const source = new ContainerDataSource(this.graphQLService, this.notificationService);
const source = new ContainerDataSource(
this.graphQLService,
this.notificationService,
this.connectionExecutionContextService
);
const dataModel = this.tableViewerStorageService.create(
source
@@ -118,7 +118,7 @@ implements IDatabaseDataModel<TOptions, TResult> {
await this.source.requestData();
}
cancel(): Promise<boolean> | boolean {
cancel(): Promise<void> | void {
return this.source.cancel();
}
@@ -8,6 +8,7 @@
import { observable, makeObservable } from 'mobx';
import type { IConnectionExecutionContext } from '@cloudbeaver/core-connections';
import { ResultDataFormat } from '@cloudbeaver/core-sdk';
import { DatabaseDataActions } from './DatabaseDataActions';
@@ -16,7 +17,6 @@ import type { IDatabaseDataActions } from './IDatabaseDataActions';
import type { IDatabaseDataEditor, IDatabaseDataResultEditor } from './IDatabaseDataEditor';
import type { IDatabaseDataResult } from './IDatabaseDataResult';
import { DatabaseDataAccessMode, IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource';
import type { IDatabaseExecutionContext } from './IDatabaseExecutionContext';
export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseDataResult>
implements IDatabaseDataSource<TOptions, TResult> {
@@ -31,7 +31,7 @@ implements IDatabaseDataSource<TOptions, TResult> {
options: TOptions | null;
requestInfo: IRequestInfo;
error: Error | null;
executionContext: IDatabaseExecutionContext | null;
executionContext: IConnectionExecutionContext | null;
abstract get canCancel(): boolean;
protected disabled: boolean;
@@ -91,7 +91,7 @@ implements IDatabaseDataSource<TOptions, TResult> {
return this.actions.get(this.results[resultIndex], action);
}
abstract cancel(): Promise<boolean> | boolean;
abstract cancel(): Promise<void> | void;
hasResult(resultIndex: number): boolean {
return resultIndex < this.results.length;
@@ -171,7 +171,7 @@ implements IDatabaseDataSource<TOptions, TResult> {
return this;
}
setExecutionContext(context: IDatabaseExecutionContext | null): this {
setExecutionContext(context: IConnectionExecutionContext | null): this {
this.executionContext = context;
return this;
}
@@ -37,6 +37,6 @@ export interface IDatabaseDataModel<TOptions = any, TResult extends IDatabaseDat
reload: () => Promise<void>;
requestDataPortion: (offset: number, count: number) => Promise<void>;
requestData: () => Promise<void>;
cancel: () => Promise<boolean> | boolean;
cancel: () => Promise<void> | void;
dispose: () => Promise<void>;
}
@@ -6,13 +6,13 @@
* you may not use this file except in compliance with the License.
*/
import type { IConnectionExecutionContext } from '@cloudbeaver/core-connections';
import type { ResultDataFormat } from '@cloudbeaver/core-sdk';
import type { IDatabaseDataAction, IDatabaseDataActionClass } from './IDatabaseDataAction';
import type { IDatabaseDataActions } from './IDatabaseDataActions';
import type { IDatabaseDataEditor, IDatabaseDataResultEditor } from './IDatabaseDataEditor';
import type { IDatabaseDataResult } from './IDatabaseDataResult';
import type { IDatabaseExecutionContext } from './IDatabaseExecutionContext';
export interface IRequestInfo {
readonly requestDuration: number;
@@ -38,7 +38,7 @@ export interface IDatabaseDataSource<TOptions, TResult extends IDatabaseDataResu
readonly options: TOptions | null;
readonly requestInfo: IRequestInfo;
readonly error: Error | null;
readonly executionContext: IDatabaseExecutionContext | null;
readonly executionContext: IConnectionExecutionContext | null;
readonly canCancel: boolean;
isReadonly: () => boolean;
@@ -63,12 +63,12 @@ export interface IDatabaseDataSource<TOptions, TResult extends IDatabaseDataResu
setOptions: (options: TOptions) => this;
setDataFormat: (dataFormat: ResultDataFormat) => this;
setSupportedDataFormats: (dataFormats: ResultDataFormat[]) => this;
setExecutionContext: (context: IDatabaseExecutionContext | null) => this;
setExecutionContext: (context: IConnectionExecutionContext | null) => this;
retry: () => Promise<void>;
requestData: () => Promise<void> | void;
saveData: () => Promise<void> | void;
cancel: () => Promise<boolean> | boolean;
cancel: () => Promise<void> | void;
clearError: () => void;
dispose: () => Promise<void>;
}
@@ -30,7 +30,6 @@ export * from './DatabaseDataModel/IDatabaseDataModel';
export * from './DatabaseDataModel/IDatabaseDataOptions';
export * from './DatabaseDataModel/IDatabaseDataResult';
export * from './DatabaseDataModel/IDatabaseDataSource';
export * from './DatabaseDataModel/IDatabaseExecutionContext';
export * from './DatabaseDataModel/IDatabaseResultSet';
export * from './DatabaseDataModel/Order';
@@ -6,12 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import type { IDatabaseExecutionContext } from '@cloudbeaver/plugin-data-viewer';
export interface ISqlQueryParams {
query: string;
executionContext: IDatabaseExecutionContext;
}
import type { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections';
export interface IResultTab {
tabId: string;
@@ -24,9 +19,9 @@ export interface IResultTab {
export interface IResultGroup {
groupId: string;
modelId: string;
sqlQueryParams: ISqlQueryParams;
order: number;
nameOrder: number;
query: string;
}
export interface ISqlEditorResultTab {
@@ -38,15 +33,15 @@ export interface ISqlEditorResultTab {
export interface IExecutionPlanTab {
tabId: string;
executionContext: IDatabaseExecutionContext;
order: number;
query: string;
options?: Record<string, any>;
}
export interface ISqlEditorTabState {
order: number;
query: string;
executionContext?: IDatabaseExecutionContext;
executionContext?: IConnectionExecutionContextInfo;
currentTabId?: string;
tabs: ISqlEditorResultTab[];
resultGroups: IResultGroup[];
@@ -9,11 +9,10 @@
import { observable, makeObservable } from 'mobx';
import type { NotificationService } from '@cloudbeaver/core-events';
import type { ITask } from '@cloudbeaver/core-executor';
import type { GraphQLService, SqlExecuteInfo } from '@cloudbeaver/core-sdk';
import { EDeferredState } from '@cloudbeaver/core-utils';
import { DatabaseDataEditor, DatabaseDataSource, IDatabaseDataOptions, IDatabaseResultSet } from '@cloudbeaver/plugin-data-viewer';
import type { SqlExecutionState } from './SqlExecutionState';
import { SQLQueryExecutionProcess } from './SqlResultTabs/SQLQueryExecutionProcess';
export interface IDataQueryOptions extends IDatabaseDataOptions {
@@ -21,42 +20,40 @@ export interface IDataQueryOptions extends IDatabaseDataOptions {
}
export class QueryDataSource extends DatabaseDataSource<IDataQueryOptions, IDatabaseResultSet> {
get canCancel(): boolean {
return this.queryExecutionProcess?.getState() === EDeferredState.PENDING;
}
currentTask: ITask<SqlExecuteInfo> | null;
queryExecutionProcess: SQLQueryExecutionProcess | null;
get canCancel(): boolean {
return this.currentTask?.cancellable || false;
}
constructor(
private graphQLService: GraphQLService,
private notificationService: NotificationService,
private sqlExecutionState: SqlExecutionState
private notificationService: NotificationService
) {
super();
makeObservable(this, {
queryExecutionProcess: observable,
currentTask: observable.ref,
});
this.queryExecutionProcess = null;
this.currentTask = null;
this.editor = new DatabaseDataEditor();
}
isDisabled(resultIndex: number): boolean {
return !this.getResult(resultIndex)?.data && this.error === null;
return (!this.getResult(resultIndex)?.data && this.error === null) || !this.executionContext?.context;
}
cancel(): boolean {
if (this.queryExecutionProcess) {
this.queryExecutionProcess.cancel();
async cancel(): Promise<void> {
if (this.currentTask) {
await this.currentTask.cancel();
}
return false;
}
async save(
prevResults: IDatabaseResultSet[]
): Promise<IDatabaseResultSet[]> {
if (!this.options || !this.executionContext) {
if (!this.options || !this.executionContext?.context) {
return prevResults;
}
@@ -70,7 +67,7 @@ export class QueryDataSource extends DatabaseDataSource<IDataQueryOptions, IData
for (const update of changes) {
const response = await this.graphQLService.sdk.updateResultsDataBatch({
connectionId: this.options.connectionId,
contextId: this.executionContext.contextId,
contextId: this.executionContext.context.id,
resultsId: update.resultId,
updatedRows: Array.from(update.diff.values()).map(diff => ({
data: diff.source,
@@ -142,29 +139,35 @@ export class QueryDataSource extends DatabaseDataSource<IDataQueryOptions, IData
async request(
prevResults: IDatabaseResultSet[]
): Promise<IDatabaseResultSet[]> {
if (!this.options || !this.executionContext) {
const options = this.options;
const executionContext = this.executionContext;
const executionContextInfo = this.executionContext?.context;
if (!options || !executionContext || !executionContextInfo) {
return prevResults;
}
const limit = this.count;
this.queryExecutionProcess = new SQLQueryExecutionProcess(this.graphQLService, this.notificationService);
const queryExecutionProcess = new SQLQueryExecutionProcess(this.graphQLService, this.notificationService);
this.sqlExecutionState.setExecutionTask(this.queryExecutionProcess);
try {
await this.queryExecutionProcess.start(
this.options.query,
this.executionContext,
this.currentTask = executionContext.run(async () => {
await queryExecutionProcess.start(
options.query,
executionContextInfo,
{
offset: this.offset,
limit,
constraints: this.options.constraints,
where: this.options.whereFilter || undefined,
constraints: options.constraints,
where: options.whereFilter || undefined,
},
this.dataFormat
);
const response = await this.queryExecutionProcess.promise;
return await queryExecutionProcess.promise;
}, () => { queryExecutionProcess.cancel(); });
try {
const response = await this.currentTask;
const results = this.getResults(response, limit);
this.clearError();
@@ -181,6 +184,6 @@ export class QueryDataSource extends DatabaseDataSource<IDataQueryOptions, IData
}
async dispose(): Promise<void> {
// TODO: this.queryExecutionProcess maybe should be disposed somehow
await this.cancel();
}
}
@@ -11,6 +11,7 @@
import { Editor, EditorConfiguration, findModeByName } from 'codemirror';
import 'codemirror/mode/sql/sql';
import 'codemirror/addon/hint/sql-hint';
import 'codemirror/addon/search/searchcursor';
import { observable, makeObservable } from 'mobx';
import type { IControlledCodeMirror } from 'react-codemirror2';
@@ -15,10 +15,11 @@ import type {
ShowHintOptions,
HintFunction,
} from 'codemirror';
import { computed, makeObservable } from 'mobx';
import { observable, computed, makeObservable, autorun } from 'mobx';
import type { IControlledCodeMirror } from 'react-codemirror2';
import type { ITab } from '@cloudbeaver/core-app';
import { ConnectionExecutionContextService } from '@cloudbeaver/core-connections';
import { IInitializableController, injectable } from '@cloudbeaver/core-di';
import type { SqlDialectInfo } from '@cloudbeaver/core-sdk';
import { throttleAsync } from '@cloudbeaver/core-utils';
@@ -28,7 +29,6 @@ import { SqlDialectInfoService } from '../SqlDialectInfoService';
import { SqlEditorService } from '../SqlEditorService';
import { SqlExecutionPlanService } from '../SqlResultTabs/ExecutionPlan/SqlExecutionPlanService';
import { SqlQueryService } from '../SqlResultTabs/SqlQueryService';
import { SqlResultTabsService } from '../SqlResultTabs/SqlResultTabsService';
const closeCharacters = /[\s()[\]{};:>,=]/;
@@ -43,7 +43,17 @@ export class SqlEditorController implements IInitializableController {
}
get isActionsDisabled(): boolean {
return this.sqlResultTabsService.getTabExecutionContext(this.tab.id).isExecuting;
if (!this.tab.handlerState.executionContext) {
return true;
}
if (this.cursor && !this.getSubQuery()) {
return true;
}
const context = this.connectionExecutionContextService.get(this.tab.handlerState.executionContext.id);
return context?.executing || false;
}
get value(): string {
@@ -83,11 +93,12 @@ export class SqlEditorController implements IInitializableController {
editorDidMount: this.handleEditorConfigure.bind(this),
};
private cursor: Position | null;
private tab!: ITab<ISqlEditorTabState>;
private editor?: Editor;
constructor(
private sqlResultTabsService: SqlResultTabsService,
private connectionExecutionContextService: ConnectionExecutionContextService,
private sqlQueryService: SqlQueryService,
private sqlDialectInfoService: SqlDialectInfoService,
private sqlEditorService: SqlEditorService,
@@ -95,16 +106,23 @@ export class SqlEditorController implements IInitializableController {
) {
this.getHandleAutocomplete = this.getHandleAutocomplete.bind(this);
this.getHandleAutocomplete = throttleAsync(this.getHandleAutocomplete, 1000 / 3);
makeObservable(this, {
this.cursor = null;
makeObservable<this, 'cursor'>(this, {
dialect: computed,
isActionsDisabled: computed,
value: computed,
cursor: observable,
});
}
init(tab: ITab<ISqlEditorTabState>): void {
this.tab = tab;
this.loadDialect();
autorun(() => {
if (this.tab.handlerState.executionContext) {
this.sqlDialectInfoService.loadSqlDialectInfo(this.tab.handlerState.executionContext.connectionId);
}
});
}
handleExecute = async (): Promise<void> => {
@@ -112,9 +130,8 @@ export class SqlEditorController implements IInitializableController {
return;
}
this.sqlQueryService.executeEditorQuery(
this.sqlResultTabsService.getTabExecutionContext(this.tab.id),
this.tab.handlerState,
await this.getExecutingQuery(),
this.getSubQuery(),
false
);
};
@@ -124,9 +141,8 @@ export class SqlEditorController implements IInitializableController {
return;
}
this.sqlQueryService.executeEditorQuery(
this.sqlResultTabsService.getTabExecutionContext(this.tab.id),
this.tab.handlerState,
await this.getExecutingQuery(),
this.getSubQuery(),
true
);
};
@@ -136,9 +152,8 @@ export class SqlEditorController implements IInitializableController {
return;
}
this.sqlExecutionPlanService.executeExecutionPlan(
this.sqlResultTabsService.getTabExecutionContext(this.tab.id),
this.tab.handlerState,
await this.getExecutingQuery(),
this.getSubQuery(),
);
};
@@ -169,15 +184,7 @@ export class SqlEditorController implements IInitializableController {
});
}
private async loadDialect(): Promise<SqlDialectInfo | undefined> {
if (!this.tab.handlerState.executionContext) {
return undefined;
}
return await this.sqlDialectInfoService.loadSqlDialectInfo(this.tab.handlerState.executionContext.connectionId);
}
private async getExecutingQuery(): Promise<string> {
private getExecutingQuery(): string {
if (!this.editor) {
return this.tab.handlerState.query;
}
@@ -187,10 +194,9 @@ export class SqlEditorController implements IInitializableController {
}
const delimiters = [];
const dialect = await this.loadDialect();
if (dialect?.scriptDelimiter) {
delimiters.push(dialect.scriptDelimiter);
if (this.dialect?.scriptDelimiter) {
delimiters.push(this.dialect.scriptDelimiter);
}
const cursor = this.editor.getCursor();
@@ -221,7 +227,7 @@ export class SqlEditorController implements IInitializableController {
let proposals = await this.sqlEditorService
.getAutocomplete(
this.tab.handlerState.executionContext.connectionId,
this.tab.handlerState.executionContext.contextId,
this.tab.handlerState.executionContext.id,
this.tab.handlerState.query,
cursorPosition,
undefined,
@@ -251,7 +257,8 @@ export class SqlEditorController implements IInitializableController {
private handleEditorConfigure(editor: Editor) {
this.editor = editor;
let cursor: Position | null = null;
let cursor: Position = editor.getCursor('from');
this.cursor = { ...cursor };
const ignoredChanges = ['+delete', 'undo', 'complete'];
@@ -275,15 +282,51 @@ export class SqlEditorController implements IInitializableController {
});
editor.on('cursorActivity', () => {
if (editor.state.completionActive) {
const newCursor = editor.getCursor('from');
const newCursor = editor.getCursor('from');
this.cursor = { ...newCursor };
if (editor.state.completionActive) {
if (newCursor.ch !== cursor?.ch || newCursor.line !== cursor.line) {
cursor = newCursor;
editor.state.completionActive.update();
}
}
this.highlightActiveQuery();
});
this.highlightActiveQuery();
}
private highlightActiveQuery() {
this.editor?.eachLine(line => {
this.editor?.removeLineClass(line, 'background', 'active-query');
});
if (this.isActionsDisabled) {
return;
}
const query = this.getSubQuery();
const search = this.editor?.getSearchCursor(query, this.editor.getCursor('from'));
if (search?.find(false) || search?.find(true)) {
const from = search.from();
const to = search.to();
for (let line = from.line; line <= to.line; line++) {
this.editor?.addLineClass(line, 'background', 'active-query');
}
}
}
private getSubQuery(): string {
const query = this.getExecutingQuery();
if (this.dialect?.scriptDelimiter && query.endsWith(this.dialect?.scriptDelimiter)) {
return query.slice(0, query.length - this.dialect.scriptDelimiter.length);
}
return query.trim();
}
private findQueryBegin(editor: Editor, delimiters: string[], position: number) {
@@ -127,9 +127,9 @@ export class SqlEditorNavigatorService {
}
if (data.type === SQLEditorNavigationAction.select) {
this.sqlEditorTabService.selectResultTab(tab, data.resultId);
this.sqlEditorTabService.selectResultTab(tab.handlerState, data.resultId);
} else if (data.type === SQLEditorNavigationAction.close) {
await this.sqlResultTabsService.removeResultTab(tab.handlerState, data.resultId, data.editorId);
await this.sqlResultTabsService.removeResultTab(tab.handlerState, data.resultId);
}
this.navigationTabsService.selectTab(tab.id);
} catch (exception) {
@@ -6,11 +6,10 @@
* you may not use this file except in compliance with the License.
*/
import { ConnectionInfoResource, ConnectionsManagerService } from '@cloudbeaver/core-connections';
import { ConnectionExecutionContextService, ConnectionsManagerService, IConnectionExecutionContext } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { GraphQLService, QuerySqlCompletionProposalsQuery } from '@cloudbeaver/core-sdk';
import type { IDatabaseExecutionContext } from '@cloudbeaver/plugin-data-viewer';
import type { ISqlEditorTabState } from './ISqlEditorTabState';
import { SqlDialectInfoService } from './SqlDialectInfoService';
@@ -19,10 +18,10 @@ import { SqlDialectInfoService } from './SqlDialectInfoService';
export class SqlEditorService {
constructor(
private gql: GraphQLService,
private connectionInfoResource: ConnectionInfoResource,
private sqlDialectInfoService: SqlDialectInfoService,
private connectionsManagerService: ConnectionsManagerService,
private notificationService: NotificationService,
private connectionExecutionContextService: ConnectionExecutionContextService
) {
}
@@ -46,7 +45,7 @@ export class SqlEditorService {
return result.sqlCompletionProposals;
}
async initEditorConnection(state: ISqlEditorTabState): Promise<IDatabaseExecutionContext | undefined> {
async initEditorConnection(state: ISqlEditorTabState): Promise<IConnectionExecutionContext | undefined> {
if (!state.executionContext) {
console.error('executeEditorQuery executionContext is not provided');
return;
@@ -54,8 +53,8 @@ export class SqlEditorService {
const context = await this.initContext(
state.executionContext.connectionId,
state.executionContext.objectCatalogId,
state.executionContext.objectSchemaId
state.executionContext.defaultCatalog,
state.executionContext.defaultSchema
);
if (!context) {
@@ -69,7 +68,7 @@ export class SqlEditorService {
connectionId?: string,
catalogId?: string,
schemaId?: string
): Promise<IDatabaseExecutionContext | null> {
): Promise<IConnectionExecutionContext | null> {
const connection = await this.connectionsManagerService.requireConnection(connectionId);
if (!connection) {
return null;
@@ -78,7 +77,7 @@ export class SqlEditorService {
try {
await this.sqlDialectInfoService.loadSqlDialectInfo(connection.id);
return await this.createSqlContext(connection.id, catalogId, schemaId);
return await this.connectionExecutionContextService.create(connection.id, catalogId, schemaId);
} catch (exception) {
this.notificationService.logException(
exception,
@@ -87,60 +86,4 @@ export class SqlEditorService {
return null;
}
}
async destroySqlContext(context: IDatabaseExecutionContext): Promise<void> {
const connection = this.connectionInfoResource.get(context.connectionId);
if (!connection?.connected) {
return;
}
try {
await this.gql.sdk.sqlContextDestroy(context);
} catch (exception) {
this.notificationService.logException(exception, `Failed to destroy SQL-context ${context.contextId}`, '', true);
}
}
/**
* Returns context id, context catalog and schema
* When try create context without catalog or schema the context is created with default catalog and schema
* and response contains its ids.
* If in the response there are no catalog or schema it means that database has no catalogs or schemas at all.
*/
async createSqlContext(
connectionId: string,
defaultCatalog?: string,
defaultSchema?: string
): Promise<IDatabaseExecutionContext> {
const response = await this.gql.sdk.sqlContextCreate({
connectionId,
defaultCatalog,
defaultSchema,
});
return {
contextId: response.context.id,
connectionId,
objectCatalogId: response.context.defaultCatalog,
objectSchemaId: response.context.defaultSchema,
};
}
/**
* Update catalog and schema for the exiting sql context in the certain connection
*/
async updateSqlContext(
connectionId: string,
contextId?: string,
defaultCatalog?: string,
defaultSchema?: string
): Promise<void> {
if (!contextId) {
throw new Error('updateSqlContext contextId not provided');
}
await this.gql.sdk.sqlContextSetDefaults({
connectionId,
contextId,
defaultCatalog,
defaultSchema,
});
}
}
@@ -17,39 +17,36 @@ import {
ITabOptions
} from '@cloudbeaver/core-app';
import {
ConnectionInfoResource,
ConnectionExecutionContextResource,
ConnectionExecutionContextService,
connectionProvider,
connectionSetter,
IConnectionExecutionContextInfo,
} from '@cloudbeaver/core-connections';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
import { MetadataMap } from '@cloudbeaver/core-utils';
import type { ISqlEditorTabState } from './ISqlEditorTabState';
import { SqlEditorPanel } from './SqlEditorPanel';
import { SqlEditorService } from './SqlEditorService';
import { SqlEditorTab } from './SqlEditorTab';
import { sqlEditorTabHandlerKey } from './sqlEditorTabHandlerKey';
import { SqlExecutionState } from './SqlExecutionState';
import { SqlResultTabsService } from './SqlResultTabs/SqlResultTabsService';
@injectable()
export class SqlEditorTabService extends Bootstrap {
readonly tabExecutionState: MetadataMap<string, SqlExecutionState>;
readonly tabHandler: TabHandler<ISqlEditorTabState>;
constructor(
private navigationTabsService: NavigationTabsService,
private notificationService: NotificationService,
private sqlEditorService: SqlEditorService,
private connectionInfo: ConnectionInfoResource,
private readonly sqlResultTabsService: SqlResultTabsService
private readonly sqlResultTabsService: SqlResultTabsService,
private connectionExecutionContextService: ConnectionExecutionContextService,
private connectionExecutionContextResource: ConnectionExecutionContextResource
) {
super();
this.tabExecutionState = new MetadataMap(() => new SqlExecutionState());
this.tabHandler = this.navigationTabsService
.registerTabHandler<ISqlEditorTabState>({
key: sqlEditorTabHandlerKey,
@@ -69,8 +66,8 @@ export class SqlEditorTabService extends Bootstrap {
}
register(): void {
this.connectionInfo.onItemDelete.addHandler(this.handleConnectionDelete.bind(this));
this.connectionInfo.onItemAdd.addHandler(this.handleConnectionUpdate.bind(this));
this.connectionExecutionContextResource.onItemAdd.addHandler(this.handleExecutionContextUpdate.bind(this));
this.connectionExecutionContextResource.onItemDelete.addHandler(this.handleExecutionContextUpdate.bind(this));
}
load(): void {}
@@ -82,7 +79,7 @@ export class SqlEditorTabService extends Bootstrap {
): Promise<ITabOptions<ISqlEditorTabState> | null> {
const executionContext = await this.sqlEditorService.initContext(connectionId, catalogId, schemaId);
if (!executionContext) {
if (!executionContext?.context) {
return null;
}
@@ -93,7 +90,7 @@ export class SqlEditorTabService extends Bootstrap {
handlerState: {
query: '',
order,
executionContext,
executionContext: { ...executionContext.context },
tabs: [],
resultGroups: [],
resultTabs: [],
@@ -102,39 +99,25 @@ export class SqlEditorTabService extends Bootstrap {
};
}
selectResultTab(tab: ITab<ISqlEditorTabState>, resultId: string): void {
tab.handlerState.currentTabId = resultId;
selectResultTab(state: ISqlEditorTabState, resultId: string): void {
state.currentTabId = resultId;
}
resetConnectionInfo(state: ISqlEditorTabState): void {
state.executionContext = undefined;
}
private async handleConnectionUpdate(key: ResourceKey<string>) {
await ResourceKeyUtils.forEachAsync(key, async key => {
const tabs = this.navigationTabsService.findTabs<ISqlEditorTabState>(
isSQLEditorTab(tab => tab.handlerState.executionContext?.connectionId === key)
);
const connection = this.connectionInfo.get(key);
private async handleExecutionContextUpdate() {
const tabs = this.navigationTabsService.findTabs<ISqlEditorTabState>(
isSQLEditorTab(tab => !!tab.handlerState.executionContext)
);
if (!connection?.connected) {
for (const tab of tabs) {
this.resetConnectionInfo(tab.handlerState);
}
}
});
}
private async handleConnectionDelete(key: ResourceKey<string>) {
await ResourceKeyUtils.forEachAsync(key, async key => {
const tabs = this.navigationTabsService.findTabs<ISqlEditorTabState>(
isSQLEditorTab(tab => tab.handlerState.executionContext?.connectionId === key)
);
for (const tab of tabs) {
for (const tab of tabs) {
const executionContext = this.connectionExecutionContextService.get(tab.handlerState.executionContext!.id);
if (!executionContext) {
this.resetConnectionInfo(tab.handlerState);
}
});
}
}
private getFreeEditorId() {
@@ -148,8 +131,9 @@ export class SqlEditorTabService extends Bootstrap {
|| typeof tab.handlerState.order !== 'number'
|| !['undefined', 'object'].includes(typeof tab.handlerState.executionContext)
|| !['string', 'undefined', 'object'].includes(typeof tab.handlerState.executionContext?.connectionId)
|| !['string', 'undefined', 'object'].includes(typeof tab.handlerState.executionContext?.contextId)
|| !['string', 'undefined', 'object'].includes(typeof tab.handlerState.executionContext?.objectCatalogId)
|| !['string', 'undefined', 'object'].includes(typeof tab.handlerState.executionContext?.id)
|| !['string', 'undefined', 'object'].includes(typeof tab.handlerState.executionContext?.defaultCatalog)
|| !['string', 'undefined', 'object'].includes(typeof tab.handlerState.executionContext?.defaultSchema)
|| !['string', 'undefined', 'object'].includes(typeof tab.handlerState.currentTabId)
|| !Array.isArray(tab.handlerState.tabs)
|| !Array.isArray(tab.handlerState.executionPlanTabs)
@@ -160,9 +144,9 @@ export class SqlEditorTabService extends Bootstrap {
}
if (tab.handlerState.executionContext) {
const connection = this.connectionInfo.get(tab.handlerState.executionContext.connectionId);
const executionContext = this.connectionExecutionContextService.get(tab.handlerState.executionContext.id);
if (!connection?.connected) {
if (!executionContext) {
this.resetConnectionInfo(tab.handlerState);
}
}
@@ -181,27 +165,28 @@ export class SqlEditorTabService extends Bootstrap {
}
private getObjectCatalogId(tab: ITab<ISqlEditorTabState>) {
return tab.handlerState.executionContext?.objectCatalogId;
return tab.handlerState.executionContext?.defaultCatalog;
}
private getObjectSchemaId(tab: ITab<ISqlEditorTabState>) {
return tab.handlerState.executionContext?.objectSchemaId;
return tab.handlerState.executionContext?.defaultSchema;
}
private async setConnectionId(connectionId: string, tab: ITab<ISqlEditorTabState>) {
try {
const context = await this.sqlEditorService.initContext(connectionId);
const executionContext = await this.sqlEditorService.initContext(connectionId);
if (!context) {
if (!executionContext?.context) {
return false;
}
if (tab.handlerState.executionContext) {
// when new context created - destroy old one silently
await this.sqlEditorService.destroySqlContext(tab.handlerState.executionContext);
const previousContext = tab.handlerState.executionContext;
tab.handlerState.executionContext = { ...executionContext.context };
if (previousContext) {
await this.destroyContext(previousContext);
}
tab.handlerState.executionContext = context;
return true;
} catch (exception) {
this.notificationService.logException(exception, 'Failed to change SQL-editor connection');
@@ -213,13 +198,19 @@ export class SqlEditorTabService extends Bootstrap {
if (!tab.handlerState.executionContext) {
return false;
}
const executionContext = this.connectionExecutionContextService.get(tab.handlerState.executionContext.id);
if (!executionContext) {
return false;
}
try {
await this.sqlEditorService.updateSqlContext(
tab.handlerState.executionContext?.connectionId,
tab.handlerState.executionContext?.contextId,
containerId
await executionContext.update(
tab.handlerState.executionContext?.id,
containerId,
);
tab.handlerState.executionContext.objectCatalogId = containerId;
tab.handlerState.executionContext.defaultCatalog = containerId;
return true;
} catch (exception) {
this.notificationService.logException(exception, 'Failed to change SQL-editor catalog');
@@ -231,14 +222,19 @@ export class SqlEditorTabService extends Bootstrap {
if (!tab.handlerState.executionContext) {
return false;
}
const executionContext = this.connectionExecutionContextService.get(tab.handlerState.executionContext.id);
if (!executionContext) {
return false;
}
try {
await this.sqlEditorService.updateSqlContext(
tab.handlerState.executionContext.connectionId,
tab.handlerState.executionContext.contextId,
tab.handlerState.executionContext.objectCatalogId,
await executionContext.update(
tab.handlerState.executionContext.defaultCatalog,
containerId
);
tab.handlerState.executionContext.objectSchemaId = containerId;
tab.handlerState.executionContext.defaultSchema = containerId;
return true;
} catch (exception) {
this.notificationService.logException(exception, 'Failed to change SQL-editor schema');
@@ -247,15 +243,23 @@ export class SqlEditorTabService extends Bootstrap {
}
private async handleTabClose(editorTab: ITab<ISqlEditorTabState>) {
this.tabExecutionState.delete(editorTab.id);
if (editorTab.handlerState.executionContext) {
const context = this.sqlResultTabsService.getTabExecutionContext(editorTab.id);
context.cancelTask();
this.sqlResultTabsService.removeTabExecutionContext(editorTab.id);
await this.sqlEditorService.destroySqlContext(editorTab.handlerState.executionContext);
await this.destroyContext(editorTab.handlerState.executionContext);
}
for (const tab of editorTab.handlerState.tabs) {
await this.sqlResultTabsService.removeResultTab(editorTab.handlerState, tab.id, editorTab.id);
await this.sqlResultTabsService.removeResultTab(editorTab.handlerState, tab.id);
}
}
private async destroyContext(contextInfo: IConnectionExecutionContextInfo) {
const executionContext = this.connectionExecutionContextService.get(contextInfo.id);
if (executionContext) {
try {
await executionContext.destroy();
} catch (exception) {
this.notificationService.logException(exception, `Failed to destroy SQL-context ${executionContext.context?.id}`, '', true);
}
}
}
}
@@ -1,52 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 { computed, observable, makeObservable } from 'mobx';
import { Deferred, EDeferredState } from '@cloudbeaver/core-utils';
export class SqlExecutionState {
constructor() {
makeObservable<SqlExecutionState, 'executionTask'>(this, {
isExecuting: computed,
canCancel: computed,
executionTask: observable.ref,
});
}
get isExecuting(): boolean {
return this.executionTask ? this.executionTask.isInProgress : false;
}
get isCanceling(): boolean {
return this.executionTask ? this.executionTask.getState() === EDeferredState.CANCELLING : false;
}
get canCancel(): boolean {
return this.executionTask ? this.executionTask.getState() === EDeferredState.PENDING : false;
}
cancelTask = () => {
if (this.executionTask) {
this.executionTask.cancel();
}
};
private executionTask: Deferred<any> | null = null;
setExecutionTask(executionTask: Deferred<any>): void {
if (this.executionTask) {
throw new Error('Simultaneous execution of several queries is forbidden');
}
this.executionTask = executionTask;
executionTask.promise.finally(() => {
this.executionTask = null;
});
}
}
@@ -6,6 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import type { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections';
import type { NotificationService } from '@cloudbeaver/core-events';
import {
AsyncTaskInfo, GraphQLService, ServerInternalError, SqlExecutionPlan
@@ -13,7 +14,6 @@ import {
import {
CancellablePromise, cancellableTimeout, Deferred, EDeferredState
} from '@cloudbeaver/core-utils';
import type { IDatabaseExecutionContext } from '@cloudbeaver/plugin-data-viewer';
const DELAY_BETWEEN_TRIES = 1000;
@@ -31,7 +31,7 @@ export class SQLExecutionPlanProcess extends Deferred<SqlExecutionPlan> {
async start(
query: string,
context: IDatabaseExecutionContext,
context: IConnectionExecutionContextInfo,
): Promise<void> {
// start async task
try {
@@ -105,11 +105,11 @@ export class SQLExecutionPlanProcess extends Deferred<SqlExecutionPlan> {
private async explainExecutionPlanAsync(
query: string,
context: IDatabaseExecutionContext,
context: IConnectionExecutionContextInfo,
): Promise<AsyncTaskInfo> {
const { taskInfo } = await this.graphQLService.sdk.asyncSqlExplainExecutionPlan({
connectionId: context.connectionId,
contextId: context.contextId,
contextId: context.id,
query,
configuration: {},
});
@@ -8,14 +8,13 @@
import { makeObservable, observable } from 'mobx';
import { ConnectionExecutionContextService } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { GraphQLService, SqlExecutionPlan } from '@cloudbeaver/core-sdk';
import { EDeferredState, uuid } from '@cloudbeaver/core-utils';
import type { ISqlEditorTabState } from '../../ISqlEditorTabState';
import { SqlDialectInfoService } from '../../SqlDialectInfoService';
import type { SqlExecutionState } from '../../SqlExecutionState';
import { SQLExecutionPlanProcess } from './SQLExecutionPlanProcess';
interface IExecutionPlanData {
@@ -28,9 +27,9 @@ export class SqlExecutionPlanService {
data: Map<string, IExecutionPlanData>;
constructor(
private sqlDialectInfoService: SqlDialectInfoService,
private graphQLService: GraphQLService,
private notificationService: NotificationService,
private connectionExecutionContextService: ConnectionExecutionContextService
) {
makeObservable(this, {
data: observable,
@@ -39,40 +38,34 @@ export class SqlExecutionPlanService {
}
async executeExecutionPlan(
executionState: SqlExecutionState,
editorState: ISqlEditorTabState,
query: string,
): Promise<void> {
if (!query.trim()) {
return;
}
const contextInfo = editorState.executionContext;
if (!editorState.executionContext) {
const executionContext = contextInfo && this.connectionExecutionContextService.get(contextInfo.id);
if (!contextInfo || !executionContext) {
console.error('executeExecutionPlan executionContext is not provided');
return;
}
const tabId = this.createExecutionPlanTab(editorState);
const subQuery = await this.getSubQuery(editorState.executionContext.connectionId, query);
const tabId = this.createExecutionPlanTab(editorState, query);
const task = new SQLExecutionPlanProcess(this.graphQLService, this.notificationService);
this.data.set(tabId, {
process: task,
executionPlan: null,
});
try {
executionState.setExecutionTask(task);
editorState.currentTabId = tabId;
await task.start(
subQuery,
{
connectionId: editorState.executionContext.connectionId,
contextId: editorState.executionContext.contextId,
}
);
const executionPlan = await executionContext.run(async () => {
await task.start(query, contextInfo);
const executionPlan = await task.promise;
return task.promise;
}, () => { task.cancel(); });
const tab = editorState.tabs.find(tab => tab.id === tabId);
@@ -91,16 +84,6 @@ export class SqlExecutionPlanService {
}
}
async getSubQuery(connectionId: string, query: string): Promise<string> {
const dialectInfo = await this.sqlDialectInfoService.loadSqlDialectInfo(connectionId);
if (dialectInfo?.scriptDelimiter && query.endsWith(dialectInfo?.scriptDelimiter)) {
return query.slice(0, query.length - dialectInfo.scriptDelimiter.length);
}
return query;
}
private removeTab(state: ISqlEditorTabState, tabId: string) {
const tab = state.tabs.find(tab => tab.id === tabId);
if (tab) {
@@ -131,7 +114,7 @@ export class SqlExecutionPlanService {
this.data.delete(tabId);
}
private createExecutionPlanTab(state: ISqlEditorTabState) {
private createExecutionPlanTab(state: ISqlEditorTabState, query: string) {
if (!state.executionContext) {
throw new Error('ExecutionContext is not provided');
}
@@ -142,11 +125,8 @@ export class SqlExecutionPlanService {
state.executionPlanTabs.push({
tabId: id,
executionContext: {
connectionId: state.executionContext.connectionId,
contextId: state.executionContext.contextId,
},
order: nameOrder,
query,
});
state.tabs.push({
@@ -6,6 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import type { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections';
import type { NotificationService } from '@cloudbeaver/core-events';
import {
AsyncTaskInfo, GraphQLService, ServerInternalError, SqlExecuteInfo, SqlDataFilter, ResultDataFormat
@@ -13,7 +14,6 @@ import {
import {
CancellablePromise, cancellableTimeout, Deferred, EDeferredState
} from '@cloudbeaver/core-utils';
import type { IDatabaseExecutionContext } from '@cloudbeaver/plugin-data-viewer';
const DELAY_BETWEEN_TRIES = 1000;
@@ -31,7 +31,7 @@ export class SQLQueryExecutionProcess extends Deferred<SqlExecuteInfo> {
async start(
query: string,
context: IDatabaseExecutionContext,
context: IConnectionExecutionContextInfo,
filter: SqlDataFilter,
dataFormat: ResultDataFormat
): Promise<void> {
@@ -107,13 +107,13 @@ export class SQLQueryExecutionProcess extends Deferred<SqlExecuteInfo> {
private async executeQueryAsync(
query: string,
context: IDatabaseExecutionContext,
context: IConnectionExecutionContextInfo,
filter: SqlDataFilter,
dataFormat: ResultDataFormat
): Promise<AsyncTaskInfo> {
const { taskInfo } = await this.graphQLService.sdk.asyncSqlExecuteQuery({
connectionId: context.connectionId,
contextId: context.contextId,
contextId: context.id,
query,
filter,
dataFormat,
@@ -6,19 +6,16 @@
* you may not use this file except in compliance with the License.
*/
import { ConnectionInfoResource } from '@cloudbeaver/core-connections';
import { ConnectionExecutionContextService, ConnectionInfoResource } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { GraphQLService } from '@cloudbeaver/core-sdk';
import { uuid, EDeferredState } from '@cloudbeaver/core-utils';
import { uuid } from '@cloudbeaver/core-utils';
import { DatabaseDataAccessMode, DataModelWrapper, TableViewerStorageService } from '@cloudbeaver/plugin-data-viewer';
import type {
IResultGroup, ISqlEditorTabState, ISqlQueryParams
} from '../ISqlEditorTabState';
import type { IResultGroup, ISqlEditorTabState } from '../ISqlEditorTabState';
import { QueryDataSource } from '../QueryDataSource';
import { SqlDialectInfoService } from '../SqlDialectInfoService';
import type { SqlExecutionState } from '../SqlExecutionState';
@injectable()
export class SqlQueryService {
@@ -28,19 +25,18 @@ export class SqlQueryService {
private graphQLService: GraphQLService,
private notificationService: NotificationService,
private connectionInfoResource: ConnectionInfoResource,
private connectionExecutionContextService: ConnectionExecutionContextService
) { }
async executeEditorQuery(
executionState: SqlExecutionState,
editorState: ISqlEditorTabState,
query: string,
inNewTab: boolean
): Promise<void> {
if (!query.trim()) {
return;
}
const contextInfo = editorState.executionContext;
const executionContext = contextInfo && this.connectionExecutionContextService.get(contextInfo.id);
if (!editorState.executionContext) {
if (!contextInfo || !executionContext) {
console.error('executeEditorQuery executionContext is not provided');
return;
}
@@ -50,27 +46,23 @@ export class SqlQueryService {
let tabGroup: IResultGroup;
let isNewTabCreated = false;
const connectionInfo = await this.connectionInfoResource.load(editorState.executionContext.connectionId);
const connectionInfo = await this.connectionInfoResource.load(contextInfo.connectionId);
const currentTab = editorState.tabs.find(tab => tab.id === editorState.currentTabId);
const resultTab = editorState.resultTabs.find(tab => tab.tabId === currentTab?.id);
const sqlQueryParams: ISqlQueryParams = {
executionContext: editorState.executionContext,
query: await this.getSubQuery(editorState.executionContext.connectionId, query),
};
if (inNewTab || !resultTab) {
source = new QueryDataSource(this.graphQLService, this.notificationService, executionState);
source = new QueryDataSource(this.graphQLService, this.notificationService);
model = this.tableViewerStorageService.create(source)
.setCountGain()
.setSlice(0);
tabGroup = this.createGroup(sqlQueryParams, editorState, model.id);
tabGroup = this.createGroup(editorState, model.id, query);
isNewTabCreated = true;
} else {
tabGroup = editorState.resultGroups.find(group => group.groupId === resultTab.groupId)!;
tabGroup.sqlQueryParams = sqlQueryParams;
tabGroup.query = query;
model = this.tableViewerStorageService.get(tabGroup.modelId)!;
source = model.source as any as QueryDataSource;
}
@@ -79,12 +71,12 @@ export class SqlQueryService {
.setAccess(connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default);
source.setOptions({
query: sqlQueryParams.query,
connectionId: sqlQueryParams.executionContext.connectionId,
query: query,
connectionId: contextInfo.connectionId,
constraints: [],
whereFilter: '',
})
.setExecutionContext(sqlQueryParams.executionContext)
.setExecutionContext(executionContext)
.setSupportedDataFormats(connectionInfo.supportedDataFormats);
this.createTabsForGroup(editorState, tabGroup, model);
@@ -119,7 +111,7 @@ export class SqlQueryService {
this.selectFirstResult(editorState, tabGroup.groupId);
} catch (exception) {
// remove first panel if execution was cancelled
if (source.queryExecutionProcess?.getState() === EDeferredState.CANCELLED && isNewTabCreated) {
if (source.currentTask?.cancelled && isNewTabCreated) {
this.removeGroup(editorState, tabGroup.groupId);
const message = `Query execution has been canceled${status ? `: ${status}` : ''}`;
this.notificationService.logException(exception, 'Query execution Error', message);
@@ -128,9 +120,9 @@ export class SqlQueryService {
}
createGroup(
params: ISqlQueryParams,
tabState: ISqlEditorTabState,
modelId: string
modelId: string,
query: string,
): IResultGroup {
const nameOrder = Math.max(1, ...tabState.resultGroups.map(group => group.nameOrder + 1));
const order = Math.max(0, ...tabState.tabs.map(tab => tab.order + 1));
@@ -141,22 +133,12 @@ export class SqlQueryService {
modelId,
order,
nameOrder,
sqlQueryParams: params,
query,
});
return tabState.resultGroups.find(group => group.groupId === groupId)!;
}
async getSubQuery(connectionId: string, query: string): Promise<string> {
const dialectInfo = await this.sqlDialectInfoService.loadSqlDialectInfo(connectionId);
if (dialectInfo?.scriptDelimiter && query.endsWith(dialectInfo?.scriptDelimiter)) {
return query.slice(0, query.length - dialectInfo.scriptDelimiter.length);
}
return query;
}
removeGroup(tabState: ISqlEditorTabState, groupId: string): void {
const tabsToRemove = tabState.resultTabs.filter(tab => tab.groupId === groupId).map(tab => tab.tabId);
@@ -7,33 +7,19 @@
*/
import { injectable } from '@cloudbeaver/core-di';
import { MetadataMap } from '@cloudbeaver/core-utils';
import type { ISqlEditorTabState } from '../ISqlEditorTabState';
import { SqlExecutionState } from '../SqlExecutionState';
import { SqlExecutionPlanService } from './ExecutionPlan/SqlExecutionPlanService';
import { SqlQueryService } from './SqlQueryService';
@injectable()
export class SqlResultTabsService {
private tabExecutionContext: MetadataMap<string, SqlExecutionState>;
constructor(
private sqlQueryService: SqlQueryService,
private sqlExecutionPlanService: SqlExecutionPlanService,
) {
this.tabExecutionContext = new MetadataMap(() => new SqlExecutionState());
}
) { }
getTabExecutionContext(tabId: string): SqlExecutionState {
return this.tabExecutionContext.get(tabId);
}
removeTabExecutionContext(tabId: string): void {
this.tabExecutionContext.delete(tabId);
}
async removeResultTab(state: ISqlEditorTabState, tabId: string, editorTabId: string): Promise<void> {
async removeResultTab(state: ISqlEditorTabState, tabId: string): Promise<void> {
const tab = state.tabs.find(tab => tab.id === tabId);
if (tab) {