Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions web-console/src/components/sql-control.scss
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,21 @@

}

.auto-complete-checkbox {
margin:10px;
.sql-control-popover {
padding:10px;
min-width: 120px;

.bp3-form-group {
margin-bottom: 0;
}

button {
span {
position: relative;
left: -7px;
}
padding-right: 35px;
}
}

.ace_tooltip {
Expand Down
34 changes: 21 additions & 13 deletions web-console/src/components/sql-control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* limitations under the License.
*/

import { Button, Checkbox, Classes, Intent, Popover, Position } from "@blueprintjs/core";
import { Button, Checkbox, Classes, FormGroup, Intent, Menu, Popover, Position } from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";
import axios from "axios";
import * as ace from 'brace';
Expand All @@ -39,6 +39,7 @@ const langTools = ace.acequire('ace/ext/language_tools');
export interface SqlControlProps extends React.Props<any> {
initSql: string | null;
onRun: (query: string) => void;
onExplain: (query: string) => void;
}

export interface SqlControlState {
Expand Down Expand Up @@ -165,19 +166,28 @@ export class SqlControl extends React.Component<SqlControlProps, SqlControlState
}

render() {
const { onRun } = this.props;
const { onRun, onExplain } = this.props;
const { query, autoCompleteOn } = this.state;

const isRune = query.trim().startsWith('{');

const autoCompletePopover = <div className={"auto-complete-checkbox"}>
<Checkbox
checked={isRune ? false : autoCompleteOn}
disabled={isRune}
label={"Auto complete"}
onChange={() => this.setState({autoCompleteOn: !autoCompleteOn})}
/>
</div>;
const SqlControlPopover = <Popover position={Position.BOTTOM_LEFT}>
<Button minimal icon={IconNames.MORE}/>
<div className={"sql-control-popover"}>
<Checkbox
checked={isRune ? false : autoCompleteOn}
label={"Auto complete"}
onChange={() => this.setState({autoCompleteOn: !autoCompleteOn})}
/>
<Button
icon={IconNames.CLEAN}
className={Classes.POPOVER_DISMISS}
text={"Explain"}
onClick={() => onExplain(query)}
minimal
/>
</div>
</Popover>;

// Set the key in the AceEditor to force a rebind and prevent an error that happens otherwise
return <div className="sql-control">
Expand Down Expand Up @@ -207,9 +217,7 @@ export class SqlControl extends React.Component<SqlControlProps, SqlControlState
<Button rightIcon={IconNames.CARET_RIGHT} onClick={() => onRun(query)}>
{isRune ? 'Rune' : 'Run'}
</Button>
<Popover position={Position.BOTTOM_LEFT} content={autoCompletePopover}>
<Button minimal icon={IconNames.MORE}/>
</Popover>
{!isRune ? SqlControlPopover : null}
</div>
</div>;
}
Expand Down
40 changes: 40 additions & 0 deletions web-console/src/dialogs/query-plan-dialog.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

.query-plan-dialog {

&.bp3-dialog {
width: 600px;
}

textarea {
width: 100%;
}

.one-query {
textarea {
height: 50vh !important;
}
}

.two-queries {
textarea {
height: 25vh !important;
}
}
}
139 changes: 139 additions & 0 deletions web-console/src/dialogs/query-plan-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Button, Classes, Dialog, FormGroup, InputGroup, TextArea } from "@blueprintjs/core";
import * as React from "react";

import { BasicQueryExplanation, SemiJoinQueryExplanation } from "../utils";

import "./query-plan-dialog.scss";

export interface QueryPlanDialogProps extends React.Props<any> {
explainResult: BasicQueryExplanation | SemiJoinQueryExplanation | string | null;
explainError: Error | null;
onClose: () => void;
}

export interface QueryPlanDialogState {

}

export class QueryPlanDialog extends React.Component<QueryPlanDialogProps, QueryPlanDialogState> {

constructor(props: QueryPlanDialogProps) {
super(props);
this.state = {};
}

render() {
const { explainResult, explainError, onClose } = this.props;

let content: JSX.Element;

if (explainError) {
content = <div>{explainError.message}</div>;
} else if (explainResult == null) {
content = <div/>;
} else if ((explainResult as BasicQueryExplanation).query) {

let signature: JSX.Element | null = null;
if ((explainResult as BasicQueryExplanation).signature) {
const signatureContent = (explainResult as BasicQueryExplanation).signature || "";
signature = <FormGroup
label={"Signature"}
>
<InputGroup defaultValue={signatureContent} readOnly/>
</FormGroup>;
}

content = <div className={"one-query"}>
<FormGroup
label={"Query"}
>
<TextArea
readOnly
value={JSON.stringify((explainResult as BasicQueryExplanation).query[0], undefined, 2)}
/>
</FormGroup>
{signature}
</div>;
} else if ((explainResult as SemiJoinQueryExplanation).mainQuery && (explainResult as SemiJoinQueryExplanation).subQueryRight) {

let mainSignature: JSX.Element | null = null;
let subSignature: JSX.Element | null = null;
if ((explainResult as SemiJoinQueryExplanation).mainQuery.signature) {
const signatureContent = (explainResult as SemiJoinQueryExplanation).mainQuery.signature || "";
mainSignature = <FormGroup
label={"Signature"}
>
<InputGroup defaultValue={signatureContent} readOnly/>
</FormGroup>;
}
if ((explainResult as SemiJoinQueryExplanation).subQueryRight.signature) {
const signatureContent = (explainResult as SemiJoinQueryExplanation).subQueryRight.signature || "";
subSignature = <FormGroup
label={"Signature"}
>
<InputGroup defaultValue={signatureContent} readOnly/>
</FormGroup>;
}

content = <div className={"two-queries"}>
<FormGroup
label={"Main query"}
>
<TextArea
readOnly
value={JSON.stringify((explainResult as SemiJoinQueryExplanation).mainQuery.query, undefined, 2)}
/>
</FormGroup>
{mainSignature}
<FormGroup
label={"Sub query"}
>
<TextArea
readOnly
value={JSON.stringify((explainResult as SemiJoinQueryExplanation).subQueryRight.query, undefined, 2)}
/>
</FormGroup>
{subSignature}
</div>;
} else {
content = <div>{explainResult}</div>;
}

return <Dialog
className={'query-plan-dialog'}
isOpen
onClose={onClose}
title={"Query plan"}
>
<div className={Classes.DIALOG_BODY}>
{content}
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button
text="Close"
onClick={onClose}
/>
</div>
</div>
</Dialog>;
}
}
67 changes: 67 additions & 0 deletions web-console/src/utils/druid-query.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,70 @@ export async function queryDruidSql(sqlQuery: Record<string, any>): Promise<any[
}
return sqlResultResp.data;
}

export interface BasicQueryExplanation {
query: any;
signature: string | null;
}

export interface SemiJoinQueryExplanation {
mainQuery: BasicQueryExplanation;
subQueryRight: BasicQueryExplanation;
}

function parseQueryPlanResult(queryPlanResult: string): BasicQueryExplanation {
if (!queryPlanResult) {
return {
query: null,
signature: null
};
}

const queryAndSignature = queryPlanResult.split(', signature=');
const queryValue = new RegExp(/query=(.+)/).exec(queryAndSignature[0]);
const signatureValue = queryAndSignature[1];

let parsedQuery: any;

if (queryValue && queryValue[1]) {
try {
parsedQuery = JSON.parse(queryValue[1]);
} catch (e) {}
}

return {
query: parsedQuery || queryPlanResult,
signature: signatureValue || null
};
}

export function parseQueryPlan(raw: string): BasicQueryExplanation | SemiJoinQueryExplanation | string {
let plan: string = raw;
plan = plan.replace(/\n/g, '');

if (plan.includes('DruidOuterQueryRel(')) {
return plan; // don't know how to parse this
}

let queryArgs: string;
const queryRelFnStart = 'DruidQueryRel(';
const semiJoinFnStart = 'DruidSemiJoin(';

if (plan.startsWith(queryRelFnStart)) {
queryArgs = plan.substring(queryRelFnStart.length, plan.length - 1);
} else if (plan.startsWith(semiJoinFnStart)) {
queryArgs = plan.substring(semiJoinFnStart.length, plan.length - 1);
const leftExpressionsArgs = ', leftExpressions=';
const keysArgumentIdx = queryArgs.indexOf(leftExpressionsArgs);
if (keysArgumentIdx !== -1) {
return {
mainQuery: parseQueryPlanResult(queryArgs.substring(0, keysArgumentIdx)),
subQueryRight: parseQueryPlan(queryArgs.substring(queryArgs.indexOf(queryRelFnStart)))
} as SemiJoinQueryExplanation;
}
} else {
return plan;
}

return parseQueryPlanResult(queryArgs);
}
1 change: 1 addition & 0 deletions web-console/src/views/sql-view.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@
flex: 1;
}
}

Loading