feat(plugin-sql-editor): CB-1060 execution plan

This commit is contained in:
Naumov Alexey
2021-06-24 10:30:17 +03:00
parent 8834f1d3fe
commit dff98db98f
26 changed files with 942 additions and 161 deletions
@@ -37,8 +37,8 @@ export interface ISqlEditorResultTab {
export interface IExecutionPlanTab {
tabId: string;
query: string;
executionContext: IDatabaseExecutionContext;
order: number;
options?: Record<string, any>;
}
@@ -50,5 +50,5 @@ export interface ISqlEditorTabState {
tabs: ISqlEditorResultTab[];
resultGroups: IResultGroup[];
resultTabs: IResultTab[];
executionPlanTabs: IExecutionPlanTab[]; // TODO: ex-plan store addition info for tab
executionPlanTabs: IExecutionPlanTab[];
}
@@ -11,7 +11,7 @@ import { PropsWithChildren, useEffect, useRef } from 'react';
import styled, { css } from 'reshadow';
import { useTab } from '@cloudbeaver/core-app';
import { StaticImage, useTab as useBaseTab } from '@cloudbeaver/core-blocks';
import { Icon, StaticImage, useTab as useBaseTab } from '@cloudbeaver/core-blocks';
import { useController } from '@cloudbeaver/core-di';
import type { SQLCodeEditorController } from './SQLCodeEditor/SQLCodeEditorController';
@@ -44,13 +44,17 @@ const styles = css`
background: none;
}
StaticImage {
StaticImage, icon-container {
padding: 4px;
height: 24px;
width: 24px;
cursor: pointer;
}
icon-container {
display: flex;
}
SQLCodeEditorLoader {
flex: 1;
}
@@ -102,11 +106,16 @@ export const SqlEditor = observer(function SqlEditor({ tabId, className }: SqlEd
/>
</button>
<button
disabled={!controller.dialect?.supportsExplainExecutionPlan}
disabled={!controller.dialect?.supportsExplainExecutionPlan || controller.isActionsDisabled}
onMouseDown={preventFocus}
onClick={controller.handleExecutionPlan}
>
plan
<icon-container title='Open execution plan'>
<Icon
name="execution-plan"
viewBox='0 0 32 32'
/>
</icon-container>
</button>
</actions>
<SQLCodeEditorLoader
@@ -19,7 +19,7 @@ import type { SqlDialectInfo } from '@cloudbeaver/core-sdk';
import type { ISqlEditorTabState } from '../ISqlEditorTabState';
import { SqlDialectInfoService } from '../SqlDialectInfoService';
import { SqlEditorService } from '../SqlEditorService';
import { SqlExecutionPlanService } from '../SqlResultTabs/SqlExecutionPlanService';
import { SqlExecutionPlanService } from '../SqlResultTabs/ExecutionPlan/SqlExecutionPlanService';
import { SqlQueryService } from '../SqlResultTabs/SqlQueryService';
import { SqlResultTabsService } from '../SqlResultTabs/SqlResultTabsService';
@@ -38,6 +38,9 @@ export class SqlEditorController implements IInitializableController {
}
handleExecute = async (): Promise<void> => {
if (this.isActionsDisabled) {
return;
}
this.sqlQueryService.executeEditorQuery(
this.sqlResultTabsService.getTabExecutionContext(this.tab.id),
this.tab.handlerState,
@@ -47,6 +50,9 @@ export class SqlEditorController implements IInitializableController {
};
handleExecuteNewTab = async (): Promise<void> => {
if (this.isActionsDisabled) {
return;
}
this.sqlQueryService.executeEditorQuery(
this.sqlResultTabsService.getTabExecutionContext(this.tab.id),
this.tab.handlerState,
@@ -56,6 +62,9 @@ export class SqlEditorController implements IInitializableController {
};
handleExecutionPlan = async (): Promise<void> => {
if (this.isActionsDisabled) {
return;
}
this.sqlExecutionPlanService.executeExecutionPlan(
this.sqlResultTabsService.getTabExecutionContext(this.tab.id),
this.tab.handlerState,
@@ -63,8 +72,6 @@ export class SqlEditorController implements IInitializableController {
);
};
// TODO: ex-plan add action for execution plan
readonly options: EditorConfiguration = {
theme: 'material',
lineNumbers: true,
@@ -80,6 +87,7 @@ export class SqlEditorController implements IInitializableController {
// Execute sql script in new tab
'Ctrl-\\': () => { this.handleExecuteNewTab(); },
'Shift-Ctrl-Enter': () => { this.handleExecuteNewTab(); },
'Shift-Ctrl-E': () => { this.handleExecutionPlan(); },
// Autocomplete
'Ctrl-Space': this.showHint.bind(this), // classic for windows, linux
@@ -246,13 +246,13 @@ export class SqlEditorTabService extends Bootstrap {
}
}
private async handleTabClose(tab: ITab<ISqlEditorTabState>) {
this.tabExecutionState.delete(tab.id);
if (tab.handlerState.executionContext) {
await this.sqlEditorService.destroySqlContext(tab.handlerState.executionContext);
private async handleTabClose(editorTab: ITab<ISqlEditorTabState>) {
this.tabExecutionState.delete(editorTab.id);
if (editorTab.handlerState.executionContext) {
await this.sqlEditorService.destroySqlContext(editorTab.handlerState.executionContext);
}
for (const resultTab of tab.handlerState.resultTabs) {
await this.sqlResultTabsService.removeResultTab(tab.handlerState, resultTab.tabId);
for (const tab of editorTab.handlerState.tabs) {
await this.sqlResultTabsService.removeResultTab(editorTab.handlerState, tab.id);
}
}
}
@@ -0,0 +1,73 @@
/*
* 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 { observable } from 'mobx';
import { useCallback, useContext, useState } from 'react';
import styled, { css } from 'reshadow';
import { Table, TableHeader, TableColumnHeader, TableBody } from '@cloudbeaver/core-blocks';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import { ExecutionPlanTreeContext, IExecutionPlanNode } from './ExecutionPlanTreeContext';
import { NestedNode } from './NestedNode';
const styles = composes(
css`
TableHeader {
composes: theme-background-surface from global;
}
`,
css`
TableHeader {
position: sticky;
top: 0;
z-index: 1;
}
`
);
interface Props {
className?: string;
}
export const ExecutionPlanTree: React.FC<Props> = function ExecutionPlanTree({ className }) {
const treeContext = useContext(ExecutionPlanTreeContext);
const [selectedNodes] = useState(() => observable(new Map()));
if (!treeContext) {
throw new Error('Tree context must be provided');
}
const selectNode = useCallback((node: IExecutionPlanNode) => {
selectedNodes.clear();
selectedNodes.set(node, true);
treeContext.selectNode(node);
}, [treeContext, selectedNodes]);
return styled(useStyles(styles))(
<Table className={className} selectedItems={selectedNodes} onSelect={selectNode}>
<TableHeader>
{treeContext.columns.map(property => (
<TableColumnHeader key={property.id || property.order} title={property.displayName}>
{property.displayName}
</TableColumnHeader>
))}
</TableHeader>
<TableBody>
{treeContext.nodes.map(node => (
<NestedNode
key={node.id}
node={node}
depth={0}
/>
))}
</TableBody>
</Table>
);
};
@@ -0,0 +1,63 @@
/*
* 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 { useContext } from 'react';
import styled, { css } from 'reshadow';
import { Split, Pane, ResizerControls, splitStyles } from '@cloudbeaver/core-blocks';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import { ExecutionPlanTree } from './ExecutionPlanTree';
import { ExecutionPlanTreeContext } from './ExecutionPlanTreeContext';
import { QueryPanel } from './QueryPanel';
const styles = composes(
css`
Pane {
composes: theme-background-surface theme-text-on-surface from global;
}
`,
css`
Split {
height: 100%;
flex-direction: column;
}
ResizerControls {
width: 100%;
height: 2px;
}
Pane:last-child {
flex: 0 0 30%;
}
`
);
interface Props {
className?: string;
}
export const ExecutionPlanTreeBlock: React.FC<Props> = function ExecutionPlanTreeBlock({ className }) {
const style = useStyles(styles, splitStyles);
const treeContext = useContext(ExecutionPlanTreeContext);
if (!treeContext) {
throw new Error('Tree context must be provided');
}
return styled(style)(
<Split className={className} sticky={30} split='horizontal' keepRatio>
<Pane>
<ExecutionPlanTree />
</Pane>
<ResizerControls />
<Pane main>
<QueryPanel />
</Pane>
</Split>
);
};
@@ -0,0 +1,26 @@
/*
* 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 { createContext } from 'react';
import type { ObjectPropertyInfo, SqlExecutionPlanNode } from '@cloudbeaver/core-sdk';
export interface IExecutionPlanNode extends SqlExecutionPlanNode {
children: IExecutionPlanNode[];
}
export interface IExecutionPlanTreeContext {
query: string;
columns: ObjectPropertyInfo[];
nodes: IExecutionPlanNode[];
selectNode: (node: IExecutionPlanNode) => void;
selectedNode: IExecutionPlanNode | null;
metadataPanel: boolean;
}
export const ExecutionPlanTreeContext = createContext<IExecutionPlanTreeContext | null>(null);
@@ -0,0 +1,48 @@
/*
* 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 styled, { css, use } from 'reshadow';
import { Icon } from '@cloudbeaver/core-blocks';
import { composes, useStyles } from '@cloudbeaver/core-theming';
interface Props {
expanded: boolean;
onClick: (event: React.MouseEvent<any, MouseEvent>) => void;
className?: string;
}
const styles = composes(
css`
Icon {
composes: theme-text-on-surface from global;
}
`,
css`
Icon {
cursor: pointer;
opacity: 0.5;
transform: rotate(-90deg);
transition: transform 0.15s ease-in-out;
&[|expanded] {
transform: rotate(0deg);
}
}
`);
export const Expand: React.FC<Props> = function Expand({ expanded, onClick, className }) {
return styled(useStyles(styles))(
<Icon
className={className}
name='arrow'
viewBox='0 0 16 16'
onClick={onClick}
{...use({ expanded })}
/>
);
};
@@ -0,0 +1,100 @@
/*
* 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 { useCallback, useContext, useState } from 'react';
import styled, { css } from 'reshadow';
import { TableItem, TableColumnValue } from '@cloudbeaver/core-blocks';
import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
import { useStyles } from '@cloudbeaver/core-theming';
import { ExecutionPlanTreeContext, IExecutionPlanNode } from './ExecutionPlanTreeContext';
import { Expand } from './Expand';
import { getPropertyValue } from './getPropertyValue';
const styles = css`
TableColumnValue {
white-space: pre;
}
expand-container {
width: 24px;
height: 24px;
margin-right: 4px;
display: flex;
}
control {
display: flex;
align-items: center;
}
`;
interface Props {
node: IExecutionPlanNode;
depth: number;
className?: string;
}
export const NestedNode: React.FC<Props> = function NestedNode({ node, depth, className }) {
const style = useStyles(styles);
const treeContext = useContext(ExecutionPlanTreeContext);
const [expanded, setExpanded] = useState(true);
if (!treeContext) {
throw new Error('Tree context must be provided');
}
const getValue = useCallback((property: ObjectPropertyInfo | undefined) => {
if (!property || !treeContext.columns.find(column => column.id === property.id)) {
return '';
}
return getPropertyValue(property);
}, [treeContext.columns]);
const expand = useCallback((event: React.MouseEvent<any, MouseEvent>) => {
event.stopPropagation();
setExpanded(prev => !prev);
}, []);
const hasChildren = node.children.length > 0;
return styled(style)(
<>
<TableItem key={`${node.id}_${depth}`} className={className} item={node}>
{treeContext.columns.map((column, idx) => {
const property = node.properties.find(property => property.id === column.id);
const value = getValue(property);
return (
<TableColumnValue key={`${property?.id}_${depth}`} title={value}>
<control>
{idx === 0 && (
<>
<span>{`${'\t'.repeat(depth)}`}</span>
<expand-container>
{hasChildren && (
<Expand expanded={expanded} onClick={expand} />
)}
</expand-container>
</>
)}
{value}
</control>
</TableColumnValue>
);
})}
</TableItem>
{expanded && node.children.map(child => (
<NestedNode
key={child.id}
node={child}
depth={depth + 1}
/>
))}
</>
);
};
@@ -0,0 +1,151 @@
/*
* 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 } from 'mobx';
import { observer } from 'mobx-react-lite';
import { useMemo, useState } from 'react';
import styled, { css } from 'reshadow';
import {
Table, TableHeader, TableColumnHeader, TableBody,
TableItem,
TableColumnValue,
TextPlaceholder,
} from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import { Expand } from './Expand';
import { getPropertyValue } from './getPropertyValue';
import { isVisibleProperty } from './useExecutionPlanTreeState';
const styles = composes(
css`
TableColumnHeader {
composes: theme-background-surface from global;
}
`,
css`
TableColumnHeader {
position: sticky;
top: 0;
z-index: 1;
}
TableColumnValue {
white-space: pre;
}
control {
display: flex;
align-items: center;
}
expand-container {
width: 24px;
height: 24px;
margin-right: 4px;
cursor: pointer;
display: flex;
}
`
);
interface Props {
properties: ObjectPropertyInfo[];
className?: string;
}
export const PropertiesPanel: React.FC<Props> = observer(function PropertiesPanel({ properties, className }) {
const style = useStyles(styles);
const translate = useTranslate();
const [generalExpanded, setGeneralExpanded] = useState(true);
const [detailsExpanded, setDetailsExpanded] = useState(true);
const { general, details } = useMemo(() => computed(() => {
const general: ObjectPropertyInfo[] = [];
const details: ObjectPropertyInfo[] = [];
for (const property of properties) {
if (isVisibleProperty(property)) {
general.push(property);
} else {
details.push(property);
}
}
return { general, details };
}), [properties]).get();
if (!general.length && !details.length) {
return <TextPlaceholder>{translate('sql_execution_plan_properties_panel_placeholder')}</TextPlaceholder>;
}
return styled(style)(
<Table className={className}>
<TableHeader>
<TableColumnHeader>
{translate('sql_execution_plan_properties_panel_name')}
</TableColumnHeader>
<TableColumnHeader>
{translate('sql_execution_plan_properties_panel_value')}
</TableColumnHeader>
</TableHeader>
<TableBody>
<TableItem item='general_root' selectDisabled>
<TableColumnValue>
<control>
<expand-container>
<Expand expanded={generalExpanded} onClick={() => setGeneralExpanded(!generalExpanded)} />
</expand-container>
{translate('sql_execution_plan_properties_panel_general')}
</control>
</TableColumnValue>
<TableColumnValue />
</TableItem>
{generalExpanded && general.map(property => {
const name = property.displayName;
const value = getPropertyValue(property);
return (
<TableItem key={property.id} item={property} selectDisabled>
<TableColumnValue title={name}>
{'\t\t' + name}
</TableColumnValue>
<TableColumnValue title={value}>
{value}
</TableColumnValue>
</TableItem>
);
})}
<TableItem item='details_root' selectDisabled>
<TableColumnValue>
<control>
<expand-container>
<Expand expanded={detailsExpanded} onClick={() => setDetailsExpanded(!detailsExpanded)} />
</expand-container>
{translate('sql_execution_plan_properties_panel_details')}
</control>
</TableColumnValue>
<TableColumnValue />
</TableItem>
{detailsExpanded && details.map(property => {
const name = property.displayName;
const value = getPropertyValue(property);
return (
<TableItem key={property.id} item={property} selectDisabled>
<TableColumnValue title={name}>
{'\t\t' + name}
</TableColumnValue>
<TableColumnValue title={value}>
{value}
</TableColumnValue>
</TableItem>
);
})}
</TableBody>
</Table>
);
});
@@ -0,0 +1,36 @@
/*
* 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 { useContext } from 'react';
import { TextareaNew } from '@cloudbeaver/core-blocks';
import { ExecutionPlanTreeContext } from './ExecutionPlanTreeContext';
interface Props {
className?: string;
}
export const QueryPanel: React.FC<Props> = function QueryPanel({ className }) {
const treeContext = useContext(ExecutionPlanTreeContext);
if (!treeContext) {
throw new Error('Tree context must be provided');
}
return (
<TextareaNew
className={className}
name='value'
rows={3}
value={treeContext.query}
readOnly
embedded
/>
);
};
@@ -8,7 +8,7 @@
import type { NotificationService } from '@cloudbeaver/core-events';
import {
AsyncTaskInfo, GraphQLService, ServerInternalError, SqlExecuteInfo, SqlDataFilter, ResultDataFormat, SqlExecutionPlan
AsyncTaskInfo, GraphQLService, ServerInternalError, SqlExecutionPlan
} from '@cloudbeaver/core-sdk';
import {
CancellablePromise, cancellableTimeout, Deferred, EDeferredState
@@ -35,7 +35,7 @@ export class SQLExecutionPlanProcess extends Deferred<SqlExecutionPlan> {
): Promise<void> {
// start async task
try {
const taskInfo = await this.executeQueryAsync(query, context);
const taskInfo = await this.explainExecutionPlanAsync(query, context);
await this.applyResult(taskInfo);
this.taskId = taskInfo.id;
if (this.getState() === EDeferredState.CANCELLING) {
@@ -57,7 +57,7 @@ export class SQLExecutionPlanProcess extends Deferred<SqlExecutionPlan> {
}
// run the first check immediately because usually the query execution is fast
try {
const taskInfo = await this.getQueryStatusAsync(this.taskId);
const taskInfo = await this.getExecutionPlanStatusAsync(this.taskId);
await this.applyResult(taskInfo);
if (this.isFinished) {
return;
@@ -103,7 +103,7 @@ export class SQLExecutionPlanProcess extends Deferred<SqlExecutionPlan> {
}
}
private async executeQueryAsync(
private async explainExecutionPlanAsync(
query: string,
context: IDatabaseExecutionContext,
): Promise<AsyncTaskInfo> {
@@ -116,7 +116,7 @@ export class SQLExecutionPlanProcess extends Deferred<SqlExecutionPlan> {
return taskInfo;
}
private async getQueryStatusAsync(taskId: string): Promise<AsyncTaskInfo> {
private async getExecutionPlanStatusAsync(taskId: string): Promise<AsyncTaskInfo> {
const { taskInfo } = await this.graphQLService.sdk.getAsyncTaskInfo({ taskId, removeOnFinish: false });
return taskInfo;
}
@@ -0,0 +1,93 @@
/*
* 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 { useCallback } from 'react';
import styled, { css } from 'reshadow';
import { Split, Pane, ResizerControls, splitStyles, TextPlaceholder } from '@cloudbeaver/core-blocks';
import { Loader } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import { EDeferredState } from '@cloudbeaver/core-utils';
import type { IExecutionPlanTab } from '../../ISqlEditorTabState';
import { ExecutionPlanTreeBlock } from './ExecutionPlanTreeBlock';
import { ExecutionPlanTreeContext } from './ExecutionPlanTreeContext';
import { PropertiesPanel } from './PropertiesPanel';
import { SqlExecutionPlanService } from './SqlExecutionPlanService';
import { useExecutionPlanTreeState } from './useExecutionPlanTreeState';
const styles = composes(
css`
Pane {
composes: theme-background-surface theme-text-on-surface from global;
}
`,
css`
Split {
height: 100%;
}
`
);
interface Props {
executionPlanTab: IExecutionPlanTab;
}
export const SqlExecutionPlanPanel: React.FC<Props> = observer(function SqlExecutionPlanPanel({
executionPlanTab,
}) {
const style = useStyles(styles, splitStyles);
const translate = useTranslate();
const sqlExecutionPlanService = useService(SqlExecutionPlanService);
const data = sqlExecutionPlanService.results.get(executionPlanTab.tabId);
const process = sqlExecutionPlanService.processes.get(executionPlanTab.tabId);
const executionPlanState = useExecutionPlanTreeState(data?.executionPlan.nodes || [], data?.executionPlan.query || '');
const loading = process?.isInProgress || !data;
const canCancel = process ? process?.getState() === EDeferredState.PENDING : false;
const cancelTask = useCallback(() => {
if (process) {
process.cancel();
}
}, [process]);
if (loading) {
return (
<Loader
cancelDisabled={!canCancel}
onCancel={cancelTask}
/>
);
}
if (!executionPlanState.nodes.length || !executionPlanState.columns.length) {
return <TextPlaceholder>{translate('sql_execution_plan_placeholder')}</TextPlaceholder>;
}
return styled(style)(
<ExecutionPlanTreeContext.Provider value={executionPlanState}>
<Split mode={executionPlanState.metadataPanel ? undefined : 'maximize'}>
<Pane main>
<ExecutionPlanTreeBlock />
</Pane>
{executionPlanState.metadataPanel && executionPlanState.selectedNode && (
<>
<ResizerControls />
<Pane>
<PropertiesPanel properties={executionPlanState.selectedNode.properties} />
</Pane>
</>
)}
</Split>
</ExecutionPlanTreeContext.Provider>
);
});
@@ -0,0 +1,146 @@
/*
* 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, toJS } from 'mobx';
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 {
query: string;
executionPlan: SqlExecutionPlan;
}
@injectable()
export class SqlExecutionPlanService {
results: Map<string, IExecutionPlanData>;
processes: Map<string, SQLExecutionPlanProcess>;
constructor(
private sqlDialectInfoService: SqlDialectInfoService,
private graphQLService: GraphQLService,
private notificationService: NotificationService,
) {
makeObservable(this, {
results: observable,
processes: observable,
});
this.results = new Map();
this.processes = new Map();
}
async executeExecutionPlan(
executionState: SqlExecutionState,
editorState: ISqlEditorTabState,
query: string,
): Promise<void> {
if (!editorState.executionContext) {
console.error('executeEditorQuery executionContext is not provided');
return;
}
const tabId = this.createExecutionPlanTab(editorState);
const subQuery = await this.getSubQuery(editorState.executionContext.connectionId, query);
const task = new SQLExecutionPlanProcess(this.graphQLService, this.notificationService);
this.processes.set(tabId, task);
executionState.setExecutionTask(task);
editorState.currentTabId = tabId;
try {
await task.start(
subQuery,
{
connectionId: editorState.executionContext?.connectionId,
contextId: editorState.executionContext?.contextId,
}
);
const executionPlan = await task.promise;
this.results.set(tabId, {
executionPlan,
query,
});
} catch (exception) {
const message = task.getState() === EDeferredState.CANCELLED ? 'Execution plan process has been canceled' : undefined;
this.notificationService.logException(exception, 'Execution plan Error', message);
this.removeTab(editorState, tabId);
}
}
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) {
this.removeExecutionPlanTab(state, tabId);
if (state.tabs.length > 0) {
state.currentTabId = state.tabs[0].id;
} else {
state.currentTabId = '';
}
}
removeExecutionPlanTab(state: ISqlEditorTabState, tabId: string): void {
const tab = state.tabs.find(tab => tab.id === tabId);
const executionPlanTab = state.executionPlanTabs.find(executionPlanTab => executionPlanTab.tabId === tabId);
if (executionPlanTab) {
state.executionPlanTabs.splice(state.executionPlanTabs.indexOf(executionPlanTab), 1);
}
if (tab) {
state.tabs.splice(state.tabs.indexOf(tab), 1);
}
this.results.delete(tabId);
this.processes.delete(tabId);
}
private createExecutionPlanTab(
state: ISqlEditorTabState,
) {
if (!state.executionContext) {
throw new Error('executeEditorQuery executionContext is not provided');
}
const id = uuid();
const order = Math.max(0, ...state.executionPlanTabs.map(tab => tab.order + 1));
state.executionPlanTabs.push({
tabId: id,
executionContext: {
connectionId: state.executionContext.connectionId,
contextId: state.executionContext.contextId,
},
order,
});
state.tabs.push({
id,
name: `Execution plan - ${order + 1}`,
icon: 'execution-plan-tab',
order,
});
return id;
}
}
@@ -0,0 +1,23 @@
/*
* 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 { ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
export function getPropertyValue(property: ObjectPropertyInfo): string {
const value = property.value;
if (!value) {
return '';
}
if (value !== null && typeof value === 'object') {
return value.displayValue || value.fullName || '';
}
return value;
}
@@ -0,0 +1,85 @@
/*
* 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 } from 'mobx';
import { useCallback, useMemo, useState } from 'react';
import { useObjectRef } from '@cloudbeaver/core-blocks';
import type { ObjectPropertyInfo, SqlExecutionPlanNode } from '@cloudbeaver/core-sdk';
import type { IExecutionPlanNode, IExecutionPlanTreeContext } from './ExecutionPlanTreeContext';
interface IState {
selectedNode: IExecutionPlanNode | null;
metadataPanel: boolean;
}
export function isVisibleProperty(property: ObjectPropertyInfo): boolean {
return property.features.includes('viewable');
}
export function useExecutionPlanTreeState(list: SqlExecutionPlanNode[], query: string): IExecutionPlanTreeContext {
const props = useObjectRef({ list, query });
const [state] = useState(() => observable<IState>({
selectedNode: null,
metadataPanel: false,
}));
const selectNode = useCallback((node: IExecutionPlanNode) => {
if (!state.metadataPanel) {
state.metadataPanel = true;
}
state.selectedNode = node;
}, [state]);
const columns = useMemo(() => computed(() => {
const result: ObjectPropertyInfo[] = [];
for (const node of props.list) {
for (const property of node.properties) {
if (property.id && isVisibleProperty(property) && !result.find(column => column.id === property.id)) {
result.push(property);
}
}
}
return result;
}), [props.list]).get();
const nodes = useMemo(() => computed(() => {
const map: Map<string, number> = new Map();
const tree: IExecutionPlanNode[] = props.list
.map((node, idx) => {
map.set(node.id, idx);
return ({ ...node, children: [] });
});
const result: IExecutionPlanNode[] = [];
for (const node of tree) {
if (node.parentId) {
const parent = map.get(node.parentId)!;
tree[parent].children.push(node);
} else {
result.push(node);
}
}
return result;
}), [props.list]).get();
return useObjectRef({
columns,
nodes,
metadataPanel: state.metadataPanel,
selectedNode: state.selectedNode,
query: props.query,
selectNode,
});
}
@@ -1,108 +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 { injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { GraphQLService } from '@cloudbeaver/core-sdk';
import { uuid } from '@cloudbeaver/core-utils';
import { TableViewerStorageService } from '@cloudbeaver/plugin-data-viewer';
import type { ISqlEditorTabState } from '../ISqlEditorTabState';
import { SqlDialectInfoService } from '../SqlDialectInfoService';
import type { SqlExecutionState } from '../SqlExecutionState';
import { SQLExecutionPlanProcess } from './SQLExecutionPlanProcess';
@injectable()
export class SqlExecutionPlanService {
constructor(
private sqlDialectInfoService: SqlDialectInfoService,
private tableViewerStorageService: TableViewerStorageService,
private graphQLService: GraphQLService,
private notificationService: NotificationService,
) { }
async executeExecutionPlan(
executionState: SqlExecutionState,
editorState: ISqlEditorTabState,
query: string,
): Promise<void> {
if (!editorState.executionContext) {
console.error('executeEditorQuery executionContext is not provided');
return;
}
const process = new SQLExecutionPlanProcess(this.graphQLService, this.notificationService);
await process.start(await this.getSubQuery(editorState.executionContext.connectionId, query),
{
connectionId: editorState.executionContext?.connectionId,
contextId: editorState.executionContext?.contextId,
}
);
const result = await process.promise;
this.createExecutionPlanTab(editorState, await this.getSubQuery(editorState.executionContext.connectionId, query));
console.log(result);
}
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;
}
removeResultTab(state: ISqlEditorTabState, tabId: string): void {
const resultTab = state.resultTabs.find(resultTab => resultTab.tabId === tabId);
const group = state.resultGroups.find(group => group.groupId === resultTab?.groupId);
if (resultTab && group) {
state.resultTabs.splice(state.resultTabs.indexOf(resultTab), 1);
const isGroupEmpty = !state.resultTabs.some(resultTab => resultTab.groupId === group.groupId);
if (isGroupEmpty) {
state.resultGroups.splice(state.resultGroups.indexOf(group), 1);
// TODO: probably we should cleanup some data before model delete
this.tableViewerStorageService.remove(group.modelId);
}
}
}
private createExecutionPlanTab(
state: ISqlEditorTabState,
query: string,
) {
if (!state.executionContext) {
console.error('executeEditorQuery executionContext is not provided');
return;
}
const id = uuid();
state.executionPlanTabs.push({
tabId: id,
executionContext: {
connectionId: state.executionContext.connectionId,
contextId: state.executionContext.contextId,
},
query,
});
state.tabs.push({
id,
name: 'Execution plan',
icon: '/icons/grid.png',
order: state.executionPlanTabs.length + 1,
});
}
}
@@ -131,7 +131,11 @@ export class SqlQueryService {
tabState: ISqlEditorTabState,
modelId: string
): IResultGroup {
const order = Math.max(0, ...tabState.tabs.map(tab => tab.order + 1));
const executionPlanIds = tabState.executionPlanTabs.map(tab => tab.tabId);
const order = Math.max(0, ...tabState.tabs
.filter(tab => !executionPlanIds.includes(tab.id))
.map(tab => tab.order + 1)
);
const groupId = uuid();
tabState.resultGroups.push({
@@ -12,6 +12,7 @@ import styled, { css } from 'reshadow';
import type { ITab } from '@cloudbeaver/core-app';
import type { ISqlEditorTabState } from '../ISqlEditorTabState';
import { SqlExecutionPlanPanel } from './ExecutionPlan/SqlExecutionPlanPanel';
import { SqlResultSetPanel } from './SqlResultSetPanel';
const style = css`
@@ -44,7 +45,7 @@ export const SqlResultPanel = observer(function SqlResultPanel({ tab, id }: SqlR
if (executionPlanTab) {
return styled(style)(
<result-panel>
<div>{executionPlanTab.query}</div>
<SqlExecutionPlanPanel executionPlanTab={executionPlanTab} />
</result-panel>
);
}
@@ -28,12 +28,16 @@ const styles = composes(
Tab {
composes: theme-ripple theme-background-surface theme-text-text-primary-on-light from global;
}
TabIcon {
composes: theme-text-surface from global;
}
tabs {
composes: theme-background-background theme-text-text-primary-on-light from global;
}
`,
css`
wrapper {
overflow: auto;
display: flex;
flex: 1;
height: 100%;
@@ -72,10 +76,25 @@ export const SqlResultTabs = observer(function SqlDataResult({ tab }: SqlDataRes
[tab]
);
const categorizedTabs = useMemo(() => computed(() => {
const resultTabs = [];
const executionPlanTabs = [];
for (const orderedTab of orderedTabs.get()) {
const resultTab = tab.handlerState.resultTabs.find(tab => tab.tabId === orderedTab.id);
if (resultTab) {
resultTabs.push(orderedTab);
continue;
}
executionPlanTabs.push(orderedTab);
}
return resultTabs.concat(executionPlanTabs);
}), [orderedTabs, tab]).get();
const handleOpen = ({ tabId }: ITabData<any>) => navigatorService.openEditorResult(tab.id, tabId);
const handleClose = ({ tabId }: ITabData<any>) => navigatorService.closeEditorResult(tab.id, tabId);
if (!tab.handlerState.resultGroups.length) {
if (!tab.handlerState.tabs.length) {
return <TextPlaceholder>{translate('sql_editor_placeholder')}</TextPlaceholder>;
}
@@ -85,7 +104,7 @@ export const SqlResultTabs = observer(function SqlDataResult({ tab }: SqlDataRes
<wrapper>
<TabsBox
currentTabId={currentId}
tabs={orderedTabs.get().map(result => (
tabs={categorizedTabs.map(result => (
<Tab key={result.id} tabId={result.id} onOpen={handleOpen} onClose={handleClose}>
<TabIcon icon={result.icon} />
<TabTitle>{result.name}</TabTitle>
@@ -93,7 +112,7 @@ export const SqlResultTabs = observer(function SqlDataResult({ tab }: SqlDataRes
))}
style={[styles]}
>
{orderedTabs.get().map(result => (
{categorizedTabs.map(result => (
<TabPanel key={result.id} tabId={result.id}>
<SqlResultPanel tab={tab} id={result.id} />
</TabPanel>
@@ -11,6 +11,7 @@ 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()
@@ -19,6 +20,7 @@ export class SqlResultTabsService {
constructor(
private sqlQueryService: SqlQueryService,
private sqlExecutionPlanService: SqlExecutionPlanService
) {
this.tabExecutionContext = new MetadataMap(() => new SqlExecutionState());
}
@@ -35,7 +37,7 @@ export class SqlResultTabsService {
}
this.sqlQueryService.removeResultTab(state, tabId);
// TODO: ex-plan add removeResultTab for ex-plan
this.sqlExecutionPlanService.removeExecutionPlanTab(state, tabId);
if (state.currentTabId === tabId) {
if (state.tabs.length > 0) {
@@ -1,3 +1,9 @@
export default [
['sql_editor_placeholder', 'Execute query with Ctrl+Enter to see results'],
['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'],
['sql_execution_plan_properties_panel_value', 'Value'],
['sql_execution_plan_properties_panel_general', 'General'],
['sql_execution_plan_properties_panel_details', 'Details'],
];
@@ -1,3 +1,9 @@
export default [
['sql_editor_placeholder', 'Нажмите Ctrl+Enter, чтобы выполнить запрос и увидеть результат'],
['sql_execution_plan_placeholder', 'План выполнения пустой'],
['sql_execution_plan_properties_panel_placeholder', 'Информация не доступна'],
['sql_execution_plan_properties_panel_name', 'Название'],
['sql_execution_plan_properties_panel_value', 'Значение'],
['sql_execution_plan_properties_panel_general', 'Общие'],
['sql_execution_plan_properties_panel_details', 'Детали'],
];
@@ -14,7 +14,7 @@ import { SqlEditorBootstrap } from './SqlEditorBootstrap';
import { SqlEditorNavigatorService } from './SqlEditorNavigatorService';
import { SqlEditorService } from './SqlEditorService';
import { SqlEditorTabService } from './SqlEditorTabService';
import { SqlExecutionPlanService } from './SqlResultTabs/SqlExecutionPlanService';
import { SqlExecutionPlanService } from './SqlResultTabs/ExecutionPlan/SqlExecutionPlanService';
import { SqlQueryService } from './SqlResultTabs/SqlQueryService';
import { SqlResultTabsService } from './SqlResultTabs/SqlResultTabsService';