Files
cloudbeaver/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlQueryService.ts
T
8a685ef47e Cb 1036 server output log sql editor (#1999)
* CB-2551. Fixed additional auth for disabled user

* CB-3923. Added output reader logs

* CB-3923. Added output reader logs

* CB-3923. Refactor after review

* CB-3923. Refactor after review

* CB-3923. Refactor after review

* CB-3923. Refactor after review

* CB-3923. Refactor after review

* CB-3923. Refactor after review

* CB-3923. Refactor after review

* CB-3923. Refactor after review

* CB-3923. Refactor after review

* CB-3924: add show output button

* CB-3924: add output event handler and resource

* CB-3924: add server output logs panel

* CB-3924: fix bugs

* CB-3923. Fixed checkstyle

* CB-3924: fixes after review

* CB-3923. Refactor after review

* CB-3924: fixes after review

* CB-3924: add line wrapper extension for editor

* CB-3923. Refactor after review

* CB-3924: show output logs for current context

* CB-3924: fixes after review

* CB-3924: add output logs icon

* CB-3924: fixes after review

* CB-3924: fixes after review

* CB-3924: fixes after review

* CB-3924: fix bug with not updated menus

* CB-3924. Fix boolean type

---------

Co-authored-by: Dmitry Osipov <teunlao@gmail.com>
Co-authored-by: EvgeniaBzzz <139753579+EvgeniaBzzz@users.noreply.github.com>
2023-09-20 13:02:29 +03:00

310 lines
12 KiB
TypeScript

/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { ConnectionExecutionContextService, ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections';
import { App, injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { AsyncTaskInfoService, GraphQLService } from '@cloudbeaver/core-sdk';
import {
DatabaseDataAccessMode,
DatabaseDataModel,
DatabaseEditAction,
DataViewerDataChangeConfirmationService,
DataViewerService,
DataViewerSettingsService,
IDatabaseDataModel,
IDatabaseResultSet,
TableViewerStorageService,
} from '@cloudbeaver/plugin-data-viewer';
import type { IResultGroup, ISqlEditorTabState } from '../ISqlEditorTabState';
import { IDataQueryOptions, QueryDataSource } from '../QueryDataSource';
import { SqlDataSourceService } from '../SqlDataSource/SqlDataSourceService';
import { SqlQueryResultService } from './SqlQueryResultService';
interface IQueryExecutionOptions {
onQueryExecutionStart?: (query: string, index: number) => void;
onQueryExecuted?: (query: string, index: number, success: boolean) => void;
}
export interface IQueryExecutionStatistics {
queries: number;
executedQueries: number;
updatedRows: number;
executeTime: number;
modelId: string | null;
}
@injectable()
export class SqlQueryService {
private readonly statisticsMap: Map<string, IQueryExecutionStatistics>;
constructor(
private readonly app: App,
private readonly tableViewerStorageService: TableViewerStorageService,
private readonly graphQLService: GraphQLService,
private readonly notificationService: NotificationService,
private readonly connectionInfoResource: ConnectionInfoResource,
private readonly connectionExecutionContextService: ConnectionExecutionContextService,
private readonly sqlQueryResultService: SqlQueryResultService,
private readonly asyncTaskInfoService: AsyncTaskInfoService,
private readonly dataViewerDataChangeConfirmationService: DataViewerDataChangeConfirmationService,
private readonly dataViewerService: DataViewerService,
private readonly sqlDataSourceService: SqlDataSourceService,
private readonly dataViewerSettingsService: DataViewerSettingsService,
) {
this.statisticsMap = new Map();
makeObservable<this, 'statisticsMap'>(this, {
statisticsMap: observable,
});
}
getStatistics(tabId: string): IQueryExecutionStatistics | undefined {
return this.statisticsMap.get(tabId);
}
initDatabaseDataModels(editorState: ISqlEditorTabState) {
const dataSource = this.sqlDataSourceService.get(editorState.editorId);
const databaseModels = dataSource?.databaseModels;
if (!databaseModels) {
return;
}
const groups = this.sqlQueryResultService.getGroups(editorState).filter(group => !databaseModels.some(model => model.id === group.modelId));
for (const group of groups) {
this.sqlQueryResultService.removeGroup(editorState, group.groupId);
}
let first = true;
for (const model of databaseModels) {
this.tableViewerStorageService.add(model);
this.dataViewerDataChangeConfirmationService.trackTableDataUpdate(model.id);
let tabGroup = this.sqlQueryResultService.getModelGroup(editorState, model.id);
if (!tabGroup) {
tabGroup = this.sqlQueryResultService.createGroup(editorState, model.id, model.source.options?.query ?? '');
this.switchTabToActiveRequest(editorState, tabGroup, model);
this.sqlQueryResultService.updateGroupTabs(editorState, model, tabGroup.groupId, first);
model.onRequest.addHandler(({ type, model }) => {
if (type === 'after') {
const tabGroup = this.sqlQueryResultService.getModelGroup(editorState, model.id);
if (tabGroup) {
this.sqlQueryResultService.updateGroupTabs(editorState, model, tabGroup.groupId);
}
}
});
first = false;
}
}
}
async executeEditorQuery(editorState: ISqlEditorTabState, query: string, inNewTab: boolean): Promise<void> {
const dataSource = this.sqlDataSourceService.get(editorState.editorId);
const contextInfo = dataSource?.executionContext;
const executionContext = contextInfo && this.connectionExecutionContextService.get(contextInfo.id);
if (!contextInfo || !executionContext) {
console.error('executeEditorQuery executionContext is not provided');
return;
}
let source: QueryDataSource;
let model: IDatabaseDataModel<IDataQueryOptions, IDatabaseResultSet>;
let isNewTabCreated = false;
const connectionKey = createConnectionParam(contextInfo.projectId, contextInfo.connectionId);
const connectionInfo = await this.connectionInfoResource.load(connectionKey);
let tabGroup = this.sqlQueryResultService.getSelectedGroup(editorState);
if (inNewTab || !tabGroup) {
source = new QueryDataSource(this.app.getServiceInjector(), this.graphQLService, this.asyncTaskInfoService);
model = this.tableViewerStorageService.add(new DatabaseDataModel(source));
this.dataViewerDataChangeConfirmationService.trackTableDataUpdate(model.id);
tabGroup = this.sqlQueryResultService.createGroup(editorState, model.id, query);
this.switchTabToActiveRequest(editorState, tabGroup, model);
isNewTabCreated = true;
} else {
model = this.tableViewerStorageService.get(tabGroup.modelId)!;
source = model.source as QueryDataSource;
tabGroup.query = query;
}
const editable = this.dataViewerService.isDataEditable(connectionInfo);
const isOutputLogsTabOpened = !!editorState.outputLogsTab;
model
.setAccess(editable ? DatabaseDataAccessMode.Default : DatabaseDataAccessMode.Readonly)
.setOptions({
query: query,
connectionKey,
constraints: [],
whereFilter: '',
readLogs: isOutputLogsTabOpened,
})
.source.setExecutionContext(executionContext)
.setSupportedDataFormats(connectionInfo.supportedDataFormats);
this.sqlQueryResultService.updateGroupTabs(editorState, model, tabGroup.groupId, true);
try {
await model.setCountGain(this.dataViewerSettingsService.getDefaultRowsCount()).setSlice(0).request();
model.setName(this.sqlQueryResultService.getTabNameForOrder(tabGroup.nameOrder, 0, model.getResults().length));
this.sqlQueryResultService.updateGroupTabs(editorState, model, tabGroup.groupId);
} catch (exception: any) {
// remove group if execution was cancelled
if (source.currentTask?.cancelled && isNewTabCreated) {
this.sqlQueryResultService.removeGroup(editorState, tabGroup.groupId);
const message = 'Query execution has been canceled';
this.notificationService.logException(exception, 'Query execution Error', message);
return;
}
throw exception;
}
}
async executeQueries(editorState: ISqlEditorTabState, queries: string[], options?: IQueryExecutionOptions): Promise<void> {
const dataSource = this.sqlDataSourceService.get(editorState.editorId);
const contextInfo = dataSource?.executionContext;
const executionContext = contextInfo && this.connectionExecutionContextService.get(contextInfo.id);
if (!contextInfo || !executionContext) {
console.error('executeEditorQuery executionContext is not provided');
return;
}
const connectionKey = createConnectionParam(contextInfo.projectId, contextInfo.connectionId);
const connectionInfo = await this.connectionInfoResource.load(connectionKey);
const statisticsTab = this.sqlQueryResultService.createStatisticsTab(editorState);
this.statisticsMap.set(statisticsTab.tabId, {
queries: queries.length,
executedQueries: 0,
executeTime: 0,
updatedRows: 0,
modelId: null,
});
editorState.currentTabId = statisticsTab.tabId;
const statistics = this.getStatistics(statisticsTab.tabId)!;
let source: QueryDataSource | undefined;
let model: IDatabaseDataModel<IDataQueryOptions, IDatabaseResultSet> | undefined;
for (let i = 0; i < queries.length; i++) {
const query = queries[i];
options?.onQueryExecutionStart?.(query, i);
if (!model || !source) {
source = new QueryDataSource(this.app.getServiceInjector(), this.graphQLService, this.asyncTaskInfoService);
model = this.tableViewerStorageService.add(new DatabaseDataModel(source));
this.dataViewerDataChangeConfirmationService.trackTableDataUpdate(model.id);
}
statistics.modelId = model.id;
const editable = this.dataViewerService.isDataEditable(connectionInfo);
const isOutputLogsTabOpened = !!editorState.outputLogsTab;
model
.setAccess(editable ? DatabaseDataAccessMode.Default : DatabaseDataAccessMode.Readonly)
.setOptions({
query: query,
connectionKey,
constraints: [],
whereFilter: '',
readLogs: isOutputLogsTabOpened,
})
.source.setExecutionContext(executionContext)
.setSupportedDataFormats(connectionInfo.supportedDataFormats);
try {
await model.setCountGain(this.dataViewerSettingsService.getDefaultRowsCount()).setSlice(0).request();
const cancelled = model.source.cancelled;
statistics.executedQueries++;
statistics.executeTime += source.requestInfo.requestDuration;
for (const result of source.results) {
statistics.updatedRows += result.updateRowCount;
}
if (source.results.some(result => result.data)) {
const tabGroup = this.sqlQueryResultService.createGroup(editorState, model.id, query);
model.setName(this.sqlQueryResultService.getTabNameForOrder(tabGroup.nameOrder, 0, model.getResults().length, statisticsTab.order, i + 1));
this.switchTabToActiveRequest(editorState, tabGroup, model);
this.sqlQueryResultService.updateGroupTabs(editorState, model, tabGroup.groupId, false, statisticsTab.order, i + 1);
model = source = undefined;
}
options?.onQueryExecuted?.(query, i, true);
if (cancelled) {
break;
}
} catch (exception: any) {
if (model) {
const tabGroup = this.sqlQueryResultService.createGroup(editorState, model.id, query);
this.sqlQueryResultService.updateGroupTabs(editorState, model, tabGroup.groupId, true, statisticsTab.order, i + 1);
model = source = undefined;
}
options?.onQueryExecuted?.(query, i, false);
break;
}
}
statistics.modelId = null;
if (model) {
this.tableViewerStorageService.remove(model.id);
}
}
removeStatisticsTab(state: ISqlEditorTabState, tabId: string): void {
this.sqlQueryResultService.removeStatisticsTab(state, tabId);
this.statisticsMap.delete(tabId);
}
private switchTabToActiveRequest(
editorState: ISqlEditorTabState,
tabGroup: IResultGroup,
model: IDatabaseDataModel<IDataQueryOptions, IDatabaseResultSet>,
) {
model.onRequest.addPostHandler(({ type }) => {
if (type === 'on') {
const activeGroupId = this.sqlQueryResultService.getSelectedGroup(editorState)?.groupId;
for (const result of model.getResults()) {
const editor = model.source.getActionImplementation(result, DatabaseEditAction);
const edited = editor?.isEdited() && model.source.executionContext?.context;
if (edited && activeGroupId !== tabGroup.groupId) {
this.sqlQueryResultService.selectResult(editorState, tabGroup.groupId, model.getResults().indexOf(result));
return;
}
}
}
});
}
}