feat: CB-945 sql script execution

This commit is contained in:
Wroud
2021-08-04 17:49:23 +03:00
parent c68216af11
commit 060703cfda
25 changed files with 544 additions and 194 deletions
@@ -209,13 +209,6 @@
<symbol id="password-show" viewBox="0 0 16 16">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 11.25C10.8247 11.25 12.6094 9.5143 13.5609 8C12.6094 6.4857 10.8247 4.75 8 4.75C5.17528 4.75 3.39058 6.4857 2.43914 8C3.39058 9.5143 5.17528 11.25 8 11.25ZM15 8C14.0667 9.8 11.866 12.5 8 12.5C4.13401 12.5 1.93333 9.8 1 8C1.93333 6.2 4.13401 3.5 8 3.5C11.866 3.5 14.0667 6.2 15 8ZM7.99997 9.88333C9.0309 9.88333 9.86663 9.0476 9.86663 8.01666C9.86663 6.98573 9.0309 6.15 7.99997 6.15C6.96904 6.15 6.1333 6.98573 6.1333 8.01666C6.1333 9.0476 6.96904 9.88333 7.99997 9.88333Z" fill="currentColor"/>
</symbol>
<symbol id="execution-plan" viewBox="0 0 24 24">
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 4C17.0886 4 17.173 4.01772 17.25 4.0498L19 4.04981V5.04981H20V8.04981H18.1501L18.1501 18.3956C18.1501 19.3068 17.4114 20.0456 16.5002 20.0456L14 20.0456V20.0498H5V17.0498H14V18.7456L16.5002 18.7456C16.6934 18.7456 16.8501 18.5889 16.8501 18.3956L16.8501 6.04981L9.15 6.0498V14.0497V14.6997H7.85V14.0497V5.65C7.85 4.73873 8.58873 4 9.5 4H17Z" fill="#338ECC"/>
<rect x="14" y="8.0498" width="2" height="2" fill="#338ECC"/>
<rect x="14" y="12.0498" width="2" height="2" fill="#338ECC"/>
<rect x="10" y="8.0498" width="2" height="2" fill="#338ECC"/>
<rect x="10" y="12.0498" width="2" height="2" fill="#338ECC"/>
</symbol>
<symbol id="execution-plan-tab" viewBox="0 0 32 32">
<path d="M5 2H19.5858L27 9.41421V30H5V2Z" stroke="#338ECC" stroke-width="2" fill="transparent"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19 1L28 10H19V1ZM8 8H13V12H8V8ZM24 22H19V26H24V22ZM19 15H24V19H19V15ZM11.5 13H9.5V25H10H11.5H18V23H11.5V18H18V16H11.5V13Z" fill="#338ECC"/>

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

@@ -22,7 +22,7 @@
@include mdc-theme-prop(color, on-surface, false);
&:hover:before {
content: '';
content: "";
position: absolute;
left: 0;
width: 100%;
@@ -30,7 +30,7 @@
z-index: -1;
opacity: 0.14;
@include mdc-theme-prop(background-color, primary, false);
}
}
}
.material .CodeMirror-hint-active {
@include mdc-theme-prop(background-color, primary, false);
@@ -89,7 +89,7 @@
.CodeMirror :global(pre).CodeMirror-line,
.CodeMirror :global(pre).CodeMirror-line-like {
padding: 0 8px;
padding: 0 16px;
}
.cm-s-material .CodeMirror-line::selection,
@@ -196,5 +196,19 @@
.cm-s-material .active-query {
background-color: $active-query;
}
.cm-s-material .running-query:before {
content: "";
position: absolute;
left: 2px;
top: 50%;
transform: translateY(-50%);
height: 0;
width: 0;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-left: 6px solid;
@include mdc-theme-prop(border-left-color, primary, false);
}
}
}
@@ -41,7 +41,8 @@ export class ContainerDataSource extends DatabaseDataSource<IDataContainerOption
}
isDisabled(resultIndex: number): boolean {
return !this.getResult(resultIndex)?.data && this.error === null;
return this.results.length === 0
|| (!this.getResult(resultIndex)?.data && this.error === null);
}
async cancel(): Promise<void> {
@@ -11,10 +11,12 @@ import { injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { GraphQLService } from '@cloudbeaver/core-sdk';
import { ContainerDataSource } from './ContainerDataSource';
import { ContainerDataSource, IDataContainerOptions } from './ContainerDataSource';
import { DatabaseDataModel } from './DatabaseDataModel/DatabaseDataModel';
import type { IDatabaseDataModel } from './DatabaseDataModel/IDatabaseDataModel';
import { DatabaseDataAccessMode } from './DatabaseDataModel/IDatabaseDataSource';
import { DataModelWrapper } from './TableViewer/DataModelWrapper';
import type { IDatabaseResultSet } from './DatabaseDataModel/IDatabaseResultSet';
import { getDefaultRowsCount } from './getDefaultRowsCount';
import { TableViewerStorageService } from './TableViewer/TableViewerStorageService';
@injectable()
@@ -37,16 +39,17 @@ export class DataViewerTableService {
async removeTableModel(tableId: string): Promise<void> {
const model = this.tableViewerStorageService.get(tableId);
if (model instanceof DataModelWrapper) {
if (model) {
await model.dispose();
this.tableViewerStorageService.remove(tableId);
}
this.tableViewerStorageService.remove(tableId);
}
async create(
connectionId: string,
containerNodePath = ''
): Promise<DataModelWrapper> {
): Promise<IDatabaseDataModel<IDataContainerOptions, IDatabaseResultSet>> {
const connectionInfo = await this.connectionInfoResource.load(connectionId);
const source = new ContainerDataSource(
this.graphQLService,
@@ -54,17 +57,17 @@ export class DataViewerTableService {
this.connectionExecutionContextService
);
const dataModel = this.tableViewerStorageService.create(
source
.setOptions({
connectionId,
containerNodePath,
constraints: [],
whereFilter: '',
})
.setSupportedDataFormats(connectionInfo.supportedDataFormats)
)
.setCountGain()
source
.setOptions({
connectionId,
containerNodePath,
constraints: [],
whereFilter: '',
})
.setSupportedDataFormats(connectionInfo.supportedDataFormats);
const dataModel = this.tableViewerStorageService.add(new DatabaseDataModel(source))
.setCountGain(getDefaultRowsCount())
.setSlice(0)
.setAccess(connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default);
@@ -1,81 +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 { DatabaseDataModel } from '../DatabaseDataModel/DatabaseDataModel';
import type { IDatabaseDataOptions } from '../DatabaseDataModel/IDatabaseDataOptions';
import type { IDatabaseDataResult } from '../DatabaseDataModel/IDatabaseDataResult';
import type { DatabaseDataAccessMode, IDatabaseDataSource } from '../DatabaseDataModel/IDatabaseDataSource';
const fetchingSettings = {
fetchMin: 100,
fetchMax: 5000,
fetchDefault: 200,
};
export class DataModelWrapper extends DatabaseDataModel<IDatabaseDataOptions, IDatabaseDataResult> {
constructor(
source: IDatabaseDataSource<any>
) {
super(source);
this.countGain = this.getDefaultRowsCount();
}
isLoading(): boolean {
return this.source.isLoading();
}
async reload(): Promise<void> {
this.setSlice(0, this.countGain);
await this.requestData();
}
async refresh(): Promise<void> {
await this.requestData();
}
setCountGain(count?: number): this {
const realCount = this.getDefaultRowsCount(count);
this.countGain = realCount;
return this;
}
setAccess(access: DatabaseDataAccessMode): this {
this.source.setAccess(access);
return this;
}
setOptions(options: any): this {
this.source.setOptions(options);
return this;
}
async requestData(): Promise<void> {
await this.source.requestData();
}
async requestDataPortion(offset: number, count: number): Promise<void> {
if (!this.isDataAvailable(offset, count)) {
this.source.setSlice(offset, count);
await this.source.requestData();
}
}
async dispose(): Promise<void> {
await this.source.dispose();
}
private getDefaultRowsCount(count?: number) {
return count !== undefined
? Math.max(
fetchingSettings.fetchMin,
Math.min(count, fetchingSettings.fetchMax)
)
: fetchingSettings.fetchDefault;
}
}
@@ -7,17 +7,16 @@
*/
import { observer } from 'mobx-react-lite';
import type { PropsWithChildren } from 'react';
import styled, { css } from 'reshadow';
import { useTranslate } from '@cloudbeaver/core-localization';
import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel';
type TableGridProps = PropsWithChildren<{
interface Props {
model: IDatabaseDataModel<any>;
resultIndex: number;
}>;
}
const styles = css`
statistics {
@@ -30,10 +29,10 @@ const styles = css`
}
`;
export const TableStatistics = observer(function TableStatistics({
export const TableStatistics: React.FC<Props> = observer(function TableStatistics({
model,
resultIndex,
}: TableGridProps) {
}) {
const translate = useTranslate();
const source = model.source;
const result = model.getResult(resultIndex);
@@ -12,8 +12,6 @@ import { injectable } from '@cloudbeaver/core-di';
import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel';
import type { IDatabaseDataResult } from '../DatabaseDataModel/IDatabaseDataResult';
import type { IDatabaseDataSource } from '../DatabaseDataModel/IDatabaseDataSource';
import { DataModelWrapper } from './DataModelWrapper';
@injectable()
export class TableViewerStorageService {
@@ -25,7 +23,7 @@ export class TableViewerStorageService {
constructor() {
makeObservable<this, 'tableModelMap'>(this, {
tableModelMap: observable,
tableModelMap: observable.shallow,
values: computed,
});
}
@@ -38,20 +36,11 @@ export class TableViewerStorageService {
return this.tableModelMap.get(tableId) as any;
}
/**
* @deprecated Use add method instead
*/
create(
source: IDatabaseDataSource<any, any>
): DataModelWrapper {
return this.add(new DataModelWrapper(source)) as DataModelWrapper;
}
add<TOptions, TResult extends IDatabaseDataResult>(
model: IDatabaseDataModel<TOptions, TResult>
): IDatabaseDataModel<TOptions, TResult> {
this.tableModelMap.set(model.id, model);
return this.tableModelMap.get(model.id)!;
return model;
}
remove(id: string): void {
@@ -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.
*/
const fetchingSettings = {
fetchMin: 100,
fetchMax: 5000,
fetchDefault: 200,
};
export function getDefaultRowsCount(count?: number): number {
return count !== undefined
? Math.max(
fetchingSettings.fetchMin,
Math.min(count, fetchingSettings.fetchMax)
)
: fetchingSettings.fetchDefault;
}
@@ -41,7 +41,6 @@ export * from './TableViewer/IDataTableActions';
export * from './TableViewer/IDataPresentationActions';
export * from './TableViewer/TableViewer';
export * from './TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService';
export * from './TableViewer/DataModelWrapper';
export * from './TableViewer/TableDataModel/TableColumn';
export * from './TableViewer/TableDataModel/TableRow';
@@ -49,5 +48,6 @@ export * from './TableViewer/TableDataModel/EditedRow';
export * from './ContainerDataSource';
export * from './DataPresentationService';
export * from './getDefaultRowsCount';
export * from './useDataModel';
export * from './ValuePanelPresentation/BooleanValue/isBooleanValuePresentationAvaliable';
@@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 27V4L21 15L10 27Z" fill="#F48400"/>
</svg>

After

Width:  |  Height:  |  Size: 152 B

@@ -0,0 +1,11 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M10 27V4L21 15L10 27Z" fill="#F48400"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.1895 23.8052L25.1727 19.1075L22.8155 19.1159L22.8239 23.7884L18.11 23.8214L18.1016 26.1786L22.8323 26.1624L22.8155 30.8924L25.1727 30.884L25.1811 26.1456L29.8781 26.1786L29.8865 23.8214L25.1895 23.8052Z" fill="#4FA843"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 552 B

@@ -0,0 +1,14 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.1501 2V10.6499H2.8501V1.60019C2.8501 0.688924 3.58883 -0.0498047 4.5001 -0.0498047H12.0001C12.0887 -0.0498047 12.1731 -0.0320862 12.2501 0H14V1H15V4H13.1501L13.1501 14.3458C13.1501 15.257 12.4114 15.9958 11.5002 15.9958L9 15.9958V16H0V13H9V14.6958L11.5002 14.6958C11.6934 14.6958 11.8501 14.5391 11.8501 14.3458L11.8501 2L4.1501 2Z" fill="#338ECC"/>
<rect x="9" y="4" width="2" height="2" fill="#338ECC"/>
<rect x="9" y="8" width="2" height="2" fill="#338ECC"/>
<rect x="5" y="4" width="2" height="2" fill="#338ECC"/>
<rect x="5" y="8" width="2" height="2" fill="#338ECC"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 855 B

@@ -0,0 +1,10 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.5 0C29.8807 0 31 1.11929 31 2.5V5H25.1001L25.1001 28C25.1001 28.4961 25.0885 29.4667 24.6934 30.3319C24.4856 30.7871 24.1578 31.2466 23.6473 31.587C23.1334 31.9298 22.5119 32.1 21.805 32.1H6.0001V32H0V26H20V29.9H21.805C22.1517 29.9 22.3314 29.8202 22.4267 29.7567C22.5254 29.6909 22.6146 29.5879 22.6922 29.4181C22.8678 29.0333 22.9001 28.5039 22.9001 28V2.6H8.9999C8.55416 2.6 8.29863 2.69185 8.15262 2.77735C8.0104 2.86064 7.90752 2.97541 7.82583 3.13249C7.63677 3.49608 7.59989 3.9996 7.5999 4.49998V23H5.3999V4.99696L5.39734 5L5.39735 3C5.39735 1.34315 6.74049 0 8.39735 0H28.5ZM19.5 15L11.5 8V22L19.5 15Z" fill="#338ECC"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 910 B

@@ -16,6 +16,11 @@ export interface IResultTab {
indexInResultSet: number;
}
export interface IStatisticsTab {
tabId: string;
order: number;
}
export interface IResultGroup {
groupId: string;
modelId: string;
@@ -46,5 +51,6 @@ export interface ISqlEditorTabState {
tabs: ISqlEditorResultTab[];
resultGroups: IResultGroup[];
resultTabs: IResultTab[];
statisticsTabs: IStatisticsTab[];
executionPlanTabs: IExecutionPlanTab[];
}
@@ -41,7 +41,9 @@ export class QueryDataSource extends DatabaseDataSource<IDataQueryOptions, IData
}
isDisabled(resultIndex: number): boolean {
return (!this.getResult(resultIndex)?.data && this.error === null) || !this.executionContext?.context;
return (!this.getResult(resultIndex)?.data && this.error === null)
|| !this.executionContext?.context
|| this.results.length === 0;
}
async cancel(): Promise<void> {
@@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite';
import { useEffect, useRef } from 'react';
import styled, { css } from 'reshadow';
import { Icon, StaticImage, useTab as useBaseTab } from '@cloudbeaver/core-blocks';
import { StaticImage, useTab as useBaseTab } from '@cloudbeaver/core-blocks';
import { useController } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import { composes, useStyles } from '@cloudbeaver/core-theming';
@@ -51,10 +51,10 @@ const styles = composes(
cursor: pointer;
}
StaticImage, Icon {
padding: 4px;
height: 24px;
width: 24px;
StaticImage {
padding: 8px;
height: 20px;
width: 20px;
cursor: pointer;
}
@@ -94,31 +94,40 @@ export const SqlEditor: React.FC<ISqlEditorProps> = observer(function SqlEditor(
disabled={controller.isActionsDisabled}
title={translate('sql_editor_sql_execution_button_tooltip')}
onMouseDown={preventFocus}
onClick={controller.handleExecute}
onClick={controller.executeQuery}
>
<StaticImage icon="/icons/sql_exec.png" />
<StaticImage icon="/icons/sql_exec.svg" />
</button>
<button
disabled={controller.isActionsDisabled}
title={translate('sql_editor_sql_execution_new_tab_button_tooltip')}
onMouseDown={preventFocus}
onClick={controller.handleExecuteNewTab}
onClick={controller.executeQueryNewTab}
>
<StaticImage icon="/icons/sql_exec_new.png" />
</button>
<button
disabled={controller.isActionsDisabled}
title={translate('sql_editor_sql_execution_script_button_tooltip')}
onMouseDown={preventFocus}
onClick={controller.executeScript}
>
<StaticImage icon="/icons/sql_script_exec.svg" />
</button>
{controller.dialect?.supportsExplainExecutionPlan && (
<button
disabled={controller.isActionsDisabled}
title={translate('sql_editor_execution_plan_button_tooltip')}
onMouseDown={preventFocus}
onClick={controller.handleExecutionPlan}
onClick={controller.showExecutionPlan}
>
<Icon name="execution-plan" viewBox='0 0 24 24' />
<StaticImage icon="/icons/sql_execution_plan.svg" />
</button>
)}
</actions>
<SQLCodeEditorLoader
ref={editor}
readonly={controller.readonly}
bindings={controller.bindings}
dialect={controller.dialect}
value={controller.value}
@@ -14,6 +14,7 @@ import type {
Hints,
ShowHintOptions,
HintFunction,
LineHandle,
} from 'codemirror';
import { observable, computed, makeObservable, autorun } from 'mobx';
import type { IControlledCodeMirror } from 'react-codemirror2';
@@ -48,12 +49,16 @@ export class SqlEditorController implements IInitializableController {
return this.sqlDialectInfoService.getDialectInfo(this.tab.handlerState.executionContext.connectionId);
}
get readonly(): boolean {
return this.executingScript;
}
get isActionsDisabled(): boolean {
if (!this.tab.handlerState.executionContext) {
return true;
}
if (this.cursor && !this.getSubQuery()) {
if (this.cursor && !this.getSubQuery().query) {
return true;
}
@@ -80,11 +85,11 @@ export class SqlEditorController implements IInitializableController {
showHint: true,
extraKeys: {
// Execute sql script
'Ctrl-Enter': () => { this.handleExecute(); },
'Ctrl-Enter': () => { this.executeQuery(); },
// Execute sql script in new tab
'Ctrl-\\': () => { this.handleExecuteNewTab(); },
'Shift-Ctrl-Enter': () => { this.handleExecuteNewTab(); },
'Shift-Ctrl-E': () => { this.handleExecutionPlan(); },
'Ctrl-\\': () => { this.executeQueryNewTab(); },
'Shift-Ctrl-Enter': () => { this.executeQueryNewTab(); },
'Shift-Ctrl-E': () => { this.showExecutionPlan(); },
// Autocomplete
'Ctrl-Space': () => { this.showHint(false); }, // classic for windows, linux
@@ -99,6 +104,7 @@ export class SqlEditorController implements IInitializableController {
editorDidMount: this.handleEditorConfigure.bind(this),
};
private executingScript: boolean;
private cursor: Position | null;
private tab!: ITab<ISqlEditorTabState>;
private editor?: Editor;
@@ -113,12 +119,15 @@ export class SqlEditorController implements IInitializableController {
this.getHandleAutocomplete = this.getHandleAutocomplete.bind(this);
this.getHandleAutocomplete = throttleAsync(this.getHandleAutocomplete, 1000 / 3);
this.cursor = null;
this.executingScript = false;
makeObservable<this, 'cursor'>(this, {
makeObservable<this, 'cursor' | 'executingScript'>(this, {
dialect: computed,
isActionsDisabled: computed,
value: computed,
cursor: observable,
executingScript: observable,
readonly: computed,
});
}
@@ -131,7 +140,7 @@ export class SqlEditorController implements IInitializableController {
});
}
handleExecute = async (): Promise<void> => {
executeQuery = async (): Promise<void> => {
if (this.isActionsDisabled) {
return;
}
@@ -142,7 +151,7 @@ export class SqlEditorController implements IInitializableController {
);
};
handleExecuteNewTab = async (): Promise<void> => {
executeQueryNewTab = async (): Promise<void> => {
if (this.isActionsDisabled) {
return;
}
@@ -153,16 +162,44 @@ export class SqlEditorController implements IInitializableController {
);
};
handleExecutionPlan = async (): Promise<void> => {
showExecutionPlan = async (): Promise<void> => {
if (this.isActionsDisabled || !this.dialect?.supportsExplainExecutionPlan) {
return;
}
this.sqlExecutionPlanService.executeExecutionPlan(
await this.sqlExecutionPlanService.executeExecutionPlan(
this.tab.handlerState,
this.getSubQuery().query,
);
};
executeScript = async (): Promise<void> => {
if (this.isActionsDisabled) {
return;
}
try {
this.executingScript = true;
const queries = this.getQueryList();
await this.sqlQueryService.executeQueries(
this.tab.handlerState,
queries.map(query => query.query),
{
onQueryExecutionStart: (query, index) => {
const subQuery = queries[index];
this.highlightExecutingLine(subQuery.begin, true);
},
onQueryExecuted: (query, index) => {
const subQuery = queries[index];
this.highlightExecutingLine(subQuery.begin, false);
},
}
);
} finally {
this.executingScript = false;
}
};
private async showHint(activeSuggest: boolean) {
if (!this.editor) {
return;
@@ -277,6 +314,49 @@ export class SqlEditorController implements IInitializableController {
return hints;
}
private getQueryList(): ISubQuery[] {
const delimiters: string[] = [];
if (this.dialect?.scriptDelimiter) {
delimiters.push(this.dialect.scriptDelimiter);
}
const lines: ISubQuery[] = [];
let begin = 0;
let subQuery = '';
this.editor?.eachLine(handle => {
subQuery += handle.text;
const trimmed = handle.text.trim();
if (this.isLineEndedWithDelimiter(delimiters, trimmed)) {
const lineNumber = this.editor?.getLineNumber(handle) || 0;
let subQueryTrimmed = subQuery.trim();
if (this.dialect?.scriptDelimiter && subQueryTrimmed.endsWith(this.dialect?.scriptDelimiter)) {
subQueryTrimmed = subQueryTrimmed
.slice(0, subQueryTrimmed.length - this.dialect.scriptDelimiter.length)
.trim();
}
if (subQueryTrimmed.length > 0) {
lines.push({
query: subQueryTrimmed,
begin,
end: lineNumber - (trimmed.length === 0 ? 1 : 0),
});
}
subQuery = '';
begin = lineNumber + 1;
}
});
return lines;
}
private handleEditorConfigure(editor: Editor) {
this.editor = editor;
@@ -322,17 +402,44 @@ export class SqlEditorController implements IInitializableController {
private highlightActiveQuery() {
this.editor?.eachLine(line => {
this.editor?.removeLineClass(line, 'background', 'active-query');
this.highlightActiveLine(line, false);
});
if (this.isActionsDisabled) {
if (!this.dialect) {
return;
}
const query = this.getSubQuery();
for (let line = query.begin; line <= query.end; line++) {
this.editor?.addLineClass(line, 'background', 'active-query');
this.highlightActiveLine(query.begin, query.end, true);
}
private highlightActiveLine(from: LineHandle, state: boolean): void
private highlightActiveLine(from: number, to: number, state: boolean): void
private highlightActiveLine(from: LineHandle | number, to: number | boolean, state?: boolean): void {
if (typeof from === 'object') {
if (state) {
this.editor?.addLineClass(from, 'background', 'active-query');
} else {
this.editor?.removeLineClass(from, 'background', 'active-query');
}
return;
}
for (let line = from; line <= to; line++) {
if (state) {
this.editor?.addLineClass(line, 'background', 'active-query');
} else {
this.editor?.removeLineClass(line, 'background', 'active-query');
}
}
}
private highlightExecutingLine(line: number, state: boolean): void {
if (state) {
this.editor?.addLineClass(line, 'background', 'running-query');
} else {
this.editor?.removeLineClass(line, 'background', 'running-query');
}
}
@@ -382,6 +489,10 @@ export class SqlEditorController implements IInitializableController {
}
private handleQueryChange(editor: Editor, data: EditorChange, query: string) {
if (this.readonly) {
(data as any).cancel();
return;
}
this.tab.handlerState.query = query;
}
}
@@ -95,6 +95,7 @@ export class SqlEditorTabService extends Bootstrap {
resultGroups: [],
resultTabs: [],
executionPlanTabs: [],
statisticsTabs: [],
},
};
}
@@ -140,6 +141,7 @@ export class SqlEditorTabService extends Bootstrap {
|| !Array.isArray(tab.handlerState.executionPlanTabs)
|| !Array.isArray(tab.handlerState.resultGroups)
|| !Array.isArray(tab.handlerState.resultTabs)
|| !Array.isArray(tab.handlerState.statisticsTabs)
) {
return false;
}
@@ -152,11 +154,13 @@ export class SqlEditorTabService extends Bootstrap {
}
}
// clean old results
tab.handlerState.currentTabId = '';
tab.handlerState.tabs = []; // clean old results
tab.handlerState.resultGroups = []; // clean old results
tab.handlerState.resultTabs = []; // clean old results
tab.handlerState.executionPlanTabs = []; // clean old results
tab.handlerState.tabs = [];
tab.handlerState.resultGroups = [];
tab.handlerState.resultTabs = [];
tab.handlerState.executionPlanTabs = [];
tab.handlerState.statisticsTabs = [];
return true;
}
@@ -8,9 +8,10 @@
import { injectable } from '@cloudbeaver/core-di';
import { uuid } from '@cloudbeaver/core-utils';
import { DataModelWrapper, TableViewerStorageService } from '@cloudbeaver/plugin-data-viewer';
import { IDatabaseDataModel, IDatabaseResultSet, TableViewerStorageService } from '@cloudbeaver/plugin-data-viewer';
import type { IResultGroup, ISqlEditorTabState } from '../ISqlEditorTabState';
import type { IResultGroup, ISqlEditorTabState, IStatisticsTab } from '../ISqlEditorTabState';
import type { IDataQueryOptions } from '../QueryDataSource';
@injectable()
export class SqlQueryResultService {
@@ -41,8 +42,9 @@ export class SqlQueryResultService {
updateGroupTabs(
editorState: ISqlEditorTabState,
model: DataModelWrapper,
groupId: string
model: IDatabaseDataModel<IDataQueryOptions, IDatabaseResultSet>,
groupId: string,
selectFirstResult?: boolean
): void {
const group = this.getGroup(editorState, groupId);
@@ -66,7 +68,9 @@ export class SqlQueryResultService {
editorState.resultTabs = editorState.resultTabs
.filter(resultTab => !tabsToRemove.includes(resultTab.tabId));
this.selectFirstResult(editorState, groupId);
if (selectFirstResult) {
this.selectFirstResult(editorState, groupId);
}
}
createGroup(
@@ -89,13 +93,45 @@ export class SqlQueryResultService {
return tabState.resultGroups.find(group => group.groupId === groupId)!;
}
createStatisticsTab(tabState: ISqlEditorTabState): IStatisticsTab {
const nameOrder = Math.max(1, ...tabState.statisticsTabs.map(group => group.order + 1));
const order = Math.max(0, ...tabState.tabs.map(tab => tab.order + 1));
const id = uuid();
tabState.statisticsTabs.push({
tabId: id,
order: nameOrder,
});
tabState.tabs.push({
id,
name: `Statistics - ${nameOrder}`,
icon: 'table-icon',
order,
});
return tabState.statisticsTabs.find(tab => tab.tabId === id)!;
}
removeStatisticsTab(tabState: ISqlEditorTabState, tabId: string): void {
tabState.tabs = tabState.tabs.filter(tab => tab.id !== tabId);
tabState.statisticsTabs = tabState.statisticsTabs.filter(tab => tab.tabId !== tabId);
}
removeGroup(tabState: ISqlEditorTabState, groupId: string): void {
const group = this.getGroup(tabState, groupId);
if (!group) {
return;
}
const tabsToRemove = tabState.resultTabs.filter(tab => tab.groupId === groupId).map(tab => tab.tabId);
tabState.tabs = tabState.tabs.filter(tab => !tabsToRemove.includes(tab.id));
tabState.resultGroups = tabState.resultGroups.filter(group => group.groupId !== groupId);
tabState.resultTabs = tabState.resultTabs.filter(tab => tab.groupId !== groupId);
tabState.currentTabId = tabState.tabs[0]?.id;
this.tableViewerStorageService.remove(group.modelId);
}
removeResultTab(state: ISqlEditorTabState, tabId: string): void {
@@ -123,35 +159,31 @@ export class SqlQueryResultService {
}
}
private selectFirstResult(editorState: ISqlEditorTabState, groupId: string, openNew = false) {
const currentTab = editorState.tabs.find(tab => tab.id === editorState.currentTabId);
private selectFirstResult(editorState: ISqlEditorTabState, groupId: string) {
const mainTab = editorState.resultTabs.filter(
resultTab => resultTab.groupId === groupId
)
.sort((a, b) => a.indexInResultSet - b.indexInResultSet);
if (mainTab.length && (!currentTab || openNew)) {
editorState.currentTabId = mainTab[0].tabId;
}
editorState.currentTabId = mainTab[0].tabId;
}
private createTabsForGroup(
state: ISqlEditorTabState,
group: IResultGroup,
model: DataModelWrapper
model: IDatabaseDataModel<IDataQueryOptions, IDatabaseResultSet>
) {
this.updateResultTab(model, state, group, 0);
this.updateResultTab(state, group, model, 0);
for (let i = 1; i < model.source.results.length; i++) {
this.updateResultTab(model, state, group, i);
this.updateResultTab(state, group, model, i);
}
}
private updateResultTab(
model: DataModelWrapper,
state: ISqlEditorTabState,
group: IResultGroup,
model: IDatabaseDataModel<IDataQueryOptions, IDatabaseResultSet>,
indexInResultSet: number,
) {
const resultTab = state.resultTabs.find(
@@ -6,18 +6,35 @@
* you may not use this file except in compliance with the License.
*/
import { makeObservable, observable } from 'mobx';
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 { DatabaseDataAccessMode, DataModelWrapper, TableViewerStorageService } from '@cloudbeaver/plugin-data-viewer';
import { DatabaseDataAccessMode, DatabaseDataModel, getDefaultRowsCount, IDatabaseDataModel, IDatabaseResultSet, TableViewerStorageService } from '@cloudbeaver/plugin-data-viewer';
import type { ISqlEditorTabState } from '../ISqlEditorTabState';
import { QueryDataSource } from '../QueryDataSource';
import type { IResultGroup, ISqlEditorTabState } from '../ISqlEditorTabState';
import { IDataQueryOptions, QueryDataSource } from '../QueryDataSource';
import { SqlQueryResultService } from './SqlQueryResultService';
interface IQueryExecutionOptions {
onQueryExecutionStart?: (query: string, index: number) => void;
onQueryExecuted?: (query: string, index: number) => void;
}
export interface IQueryExecutionStatistics {
queries: number;
executedQueries: number;
currentQuery: string | null;
updatedRows: number;
executeTime: number;
}
@injectable()
export class SqlQueryService {
private statisticsMap: Map<string, IQueryExecutionStatistics>;
constructor(
private tableViewerStorageService: TableViewerStorageService,
private graphQLService: GraphQLService,
@@ -25,7 +42,17 @@ export class SqlQueryService {
private connectionInfoResource: ConnectionInfoResource,
private connectionExecutionContextService: ConnectionExecutionContextService,
private sqlQueryResultService: SqlQueryResultService
) { }
) {
this.statisticsMap = new Map();
makeObservable<this, 'statisticsMap'>(this, {
statisticsMap: observable,
});
}
getStatistics(tabId: string): IQueryExecutionStatistics | undefined {
return this.statisticsMap.get(tabId);
}
async executeEditorQuery(
editorState: ISqlEditorTabState,
@@ -41,7 +68,7 @@ export class SqlQueryService {
}
let source: QueryDataSource;
let model: DataModelWrapper;
let model: IDatabaseDataModel<IDataQueryOptions, IDatabaseResultSet>;
let isNewTabCreated = false;
const connectionInfo = await this.connectionInfoResource.load(contextInfo.connectionId);
@@ -49,43 +76,39 @@ export class SqlQueryService {
if (inNewTab || !tabGroup) {
source = new QueryDataSource(this.graphQLService, this.notificationService);
model = this.tableViewerStorageService.create(source)
.setCountGain()
.setSlice(0);
model = this.tableViewerStorageService.add(new DatabaseDataModel(source));
tabGroup = this.sqlQueryResultService.createGroup(editorState, model.id, query);
isNewTabCreated = true;
} else {
tabGroup.query = query;
model = this.tableViewerStorageService.get(tabGroup.modelId)!;
source = model.source as any as QueryDataSource;
source = model.source as QueryDataSource;
tabGroup.query = query;
}
model
.setAccess(connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default);
source.setOptions({
query: query,
connectionId: contextInfo.connectionId,
constraints: [],
whereFilter: '',
})
.setAccess(connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default)
.setOptions({
query: query,
connectionId: contextInfo.connectionId,
constraints: [],
whereFilter: '',
})
.source
.setExecutionContext(executionContext)
.setSupportedDataFormats(connectionInfo.supportedDataFormats);
this.sqlQueryResultService.updateGroupTabs(editorState, model, tabGroup.groupId);
this.sqlQueryResultService.updateGroupTabs(editorState, model, tabGroup.groupId, true);
try {
await model
.setCountGain()
.setCountGain(getDefaultRowsCount())
.setSlice(0)
.requestData();
this.sqlQueryResultService.updateGroupTabs(editorState, model, tabGroup.groupId);
} catch (exception) {
// remove first panel if execution was cancelled
// remove group if execution was cancelled
if (source.currentTask?.cancelled && isNewTabCreated) {
this.sqlQueryResultService.removeGroup(editorState, tabGroup.groupId);
const message = `Query execution has been canceled${status ? `: ${status}` : ''}`;
@@ -93,4 +116,100 @@ export class SqlQueryService {
}
}
}
async executeQueries(
editorState: ISqlEditorTabState,
queries: string[],
options?: IQueryExecutionOptions
): Promise<void> {
const contextInfo = editorState.executionContext;
const executionContext = contextInfo && this.connectionExecutionContextService.get(contextInfo.baseId);
if (!contextInfo || !executionContext) {
console.error('executeEditorQuery executionContext is not provided');
return;
}
let source: QueryDataSource;
let model: IDatabaseDataModel<IDataQueryOptions, IDatabaseResultSet>;
const connectionInfo = await this.connectionInfoResource.load(contextInfo.connectionId);
let tabGroup: IResultGroup | undefined;
const statisticsTab = this.sqlQueryResultService.createStatisticsTab(editorState);
this.statisticsMap.set(statisticsTab.tabId, {
queries: queries.length,
currentQuery: null,
executedQueries: 0,
executeTime: 0,
updatedRows: 0,
});
editorState.currentTabId = statisticsTab.tabId;
const statistics = this.getStatistics(statisticsTab.tabId)!;
for (let i = 0; i < queries.length; i++) {
const query = queries[i];
statistics.currentQuery = query;
options?.onQueryExecutionStart?.(query, i);
if (!tabGroup) {
source = new QueryDataSource(this.graphQLService, this.notificationService);
model = this.tableViewerStorageService.add(new DatabaseDataModel(source));
tabGroup = this.sqlQueryResultService.createGroup(editorState, model.id, query);
} else {
tabGroup.query = query;
model = this.tableViewerStorageService.get(tabGroup.modelId)!;
source = model.source as QueryDataSource;
}
model
.setAccess(connectionInfo.readOnly ? DatabaseDataAccessMode.Readonly : DatabaseDataAccessMode.Default)
.setOptions({
query: query,
connectionId: contextInfo.connectionId,
constraints: [],
whereFilter: '',
})
.source
.setExecutionContext(executionContext)
.setSupportedDataFormats(connectionInfo.supportedDataFormats);
this.sqlQueryResultService.updateGroupTabs(editorState, model, tabGroup.groupId);
try {
await model
.setCountGain(getDefaultRowsCount())
.setSlice(0)
.requestData();
statistics.executedQueries++;
statistics.executeTime += source.requestInfo.requestDuration;
for (const result of source.results) {
statistics.updatedRows += result.updateRowCount;
}
this.sqlQueryResultService.updateGroupTabs(editorState, model, tabGroup.groupId);
if (source.results.some(result => result.data)) {
tabGroup = undefined;
} else if (i === queries.length - 1) {
this.sqlQueryResultService.removeGroup(editorState, tabGroup.groupId);
}
} catch (exception) {
break;
} finally {
options?.onQueryExecuted?.(query, i);
}
}
statistics.currentQuery = null;
}
removeStatisticsTab(state: ISqlEditorTabState, tabId: string): void {
this.sqlQueryResultService.removeStatisticsTab(state, tabId);
this.statisticsMap.delete(tabId);
}
}
@@ -14,6 +14,7 @@ import type { ITab } from '@cloudbeaver/core-app';
import type { ISqlEditorTabState } from '../ISqlEditorTabState';
import { SqlExecutionPlanPanel } from './ExecutionPlan/SqlExecutionPlanPanel';
import { SqlResultSetPanel } from './SqlResultSetPanel';
import { SqlScriptStatisticsPanel } from './SqlScriptStatisticsPanel';
const style = css`
result-panel {
@@ -51,5 +52,15 @@ export const SqlResultPanel = observer(function SqlResultPanel({ tab, id }: SqlR
);
}
const statisticsTab = tab.handlerState.statisticsTabs.find(tab => tab.tabId === id);
if (statisticsTab) {
return styled(style)(
<result-panel>
<SqlScriptStatisticsPanel tab={statisticsTab} />
</result-panel>
);
}
return null;
});
@@ -11,10 +11,12 @@ import { injectable } from '@cloudbeaver/core-di';
import type { ISqlEditorTabState } from '../ISqlEditorTabState';
import { SqlExecutionPlanService } from './ExecutionPlan/SqlExecutionPlanService';
import { SqlQueryResultService } from './SqlQueryResultService';
import { SqlQueryService } from './SqlQueryService';
@injectable()
export class SqlResultTabsService {
constructor(
private sqlQueryService: SqlQueryService,
private sqlQueryResultService: SqlQueryResultService,
private sqlExecutionPlanService: SqlExecutionPlanService,
) { }
@@ -26,6 +28,7 @@ export class SqlResultTabsService {
state.tabs.splice(state.tabs.indexOf(tab), 1);
}
this.sqlQueryService.removeStatisticsTab(state, tabId);
this.sqlQueryResultService.removeResultTab(state, tabId);
this.sqlExecutionPlanService.removeExecutionPlanTab(state, tabId);
@@ -0,0 +1,67 @@
/*
* 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 { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { Loader, TextPlaceholder } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import type { IStatisticsTab } from '../ISqlEditorTabState';
import { SqlQueryService } from './SqlQueryService';
interface IProps {
tab: IStatisticsTab;
}
const styles = css`
statistics {
composes: theme-typography--caption from global;
flex: 1;
overflow: auto;
box-sizing: border-box;
white-space: pre-wrap;
padding: 16px;
}
box {
display: flex;
align-items: center;
}
icon {
width: 16px;
height: 16px;
margin-right: 8px;
}
`;
export const SqlScriptStatisticsPanel: React.FC<IProps> = observer(function SqlScriptStatisticsPanel({
tab,
}) {
const sqlQueryService = useService(SqlQueryService);
const statistics = sqlQueryService.getStatistics(tab.tabId);
const translate = useTranslate();
if (!statistics) {
return <TextPlaceholder>{translate('sql_editor_sql_statistics_unavailable')}</TextPlaceholder>;
}
return styled(styles)(
<statistics as='div'>
{translate('sql_editor_sql_execution_queries')} {statistics.executedQueries} / {statistics.queries}<br />
{translate('data_viewer_statistics_duration')} {statistics.executeTime} ms<br />
{translate('data_viewer_statistics_updated_rows')} {statistics.updatedRows}<br />
{statistics.currentQuery && (
<>
<h3><box><icon><Loader small fullSize /></icon>{translate('sql_editor_sql_execution_executing')}</box></h3>
<pre>{statistics.currentQuery}</pre>
</>
)}
</statistics>
);
});
@@ -3,6 +3,10 @@ export default [
['sql_editor_execution_plan_button_tooltip', 'Explain execution plan (Shift + Ctrl + E)'],
['sql_editor_sql_execution_button_tooltip', 'Execute SQL Statement (Ctrl + Enter)'],
['sql_editor_sql_execution_new_tab_button_tooltip', 'Execute SQL Statement in new tab (Ctrl + \\)(Shift + Ctrl + Enter)'],
['sql_editor_sql_execution_script_button_tooltip', 'Execute SQL Script (Alt + X)'],
['sql_editor_sql_statistics_unavailable', 'Statistics is not available'],
['sql_editor_sql_execution_queries', 'Queries:'],
['sql_editor_sql_execution_executing', 'Executing...'],
['sql_execution_plan_placeholder', 'Execution plan is empty'],
['sql_execution_plan_properties_panel_placeholder', 'There are no items to show'],
['sql_execution_plan_properties_panel_name', 'Name'],
@@ -3,6 +3,10 @@ export default [
['sql_editor_execution_plan_button_tooltip', 'Посмотреть информацию о плане выполнения запроса (Shift + Ctrl + E)'],
['sql_editor_sql_execution_button_tooltip', 'Выполнить SQL Выражение (Ctrl + Enter)'],
['sql_editor_sql_execution_new_tab_button_tooltip', 'Выполнить SQL Выражение в новой вкладке (Ctrl + \\)(Shift + Ctrl + Enter)'],
['sql_editor_sql_execution_script_button_tooltip', 'Исполнить SQL Скрипт (Alt + X)'],
['sql_editor_sql_statistics_unavailable', 'Статистика не доступна'],
['sql_editor_sql_execution_queries', 'Запросы:'],
['sql_editor_sql_execution_executing', 'Выполняется...'],
['sql_execution_plan_placeholder', 'План выполнения пуст'],
['sql_execution_plan_properties_panel_placeholder', 'Информация не доступна'],
['sql_execution_plan_properties_panel_name', 'Название'],