Web console: make console's Explain more honest (#8327)

* fix structure

* update snapshots
This commit is contained in:
Vadim Ogievetsky 2019-08-16 15:51:49 -07:00 committed by Clint Wylie
parent 7362b1d8fc
commit 0d69438395
6 changed files with 144 additions and 145 deletions

View File

@ -49,6 +49,8 @@ exports[`sql view matches snapshot 1`] = `
queryContext={Object {}}
runeMode={false}
setAutoRun={[Function]}
setWrapQuery={[Function]}
wrapQuery={true}
/>
</div>
</div>

View File

@ -45,7 +45,7 @@ import './column-tree.scss';
function handleTableClick(
tableSchema: string,
nodeData: ITreeNode,
onQueryStringChange: (queryString: string) => void,
onQueryStringChange: (queryString: string, run: boolean) => void,
): void {
let columns: string[];
if (nodeData.childNodes) {
@ -54,12 +54,18 @@ function handleTableClick(
columns = ['*'];
}
if (tableSchema === 'druid') {
onQueryStringChange(`SELECT ${columns.join(', ')}
onQueryStringChange(
`SELECT ${columns.join(', ')}
FROM ${escapeSqlIdentifier(String(nodeData.label))}
WHERE "__time" >= CURRENT_TIMESTAMP - INTERVAL '1' DAY`);
WHERE "__time" >= CURRENT_TIMESTAMP - INTERVAL '1' DAY`,
true,
);
} else {
onQueryStringChange(`SELECT ${columns.join(', ')}
FROM ${tableSchema}.${nodeData.label}`);
onQueryStringChange(
`SELECT ${columns.join(', ')}
FROM ${tableSchema}.${nodeData.label}`,
true,
);
}
}
@ -67,40 +73,49 @@ function handleColumnClick(
columnSchema: string,
columnTable: string,
nodeData: ITreeNode,
onQueryStringChange: (queryString: string) => void,
onQueryStringChange: (queryString: string, run: boolean) => void,
): void {
if (columnSchema === 'druid') {
if (nodeData.icon === IconNames.TIME) {
onQueryStringChange(`SELECT
onQueryStringChange(
`SELECT
TIME_FLOOR(${escapeSqlIdentifier(String(nodeData.label))}, 'PT1H') AS "Time",
COUNT(*) AS "Count"
FROM ${escapeSqlIdentifier(columnTable)}
WHERE "__time" >= CURRENT_TIMESTAMP - INTERVAL '1' DAY
GROUP BY 1
ORDER BY "Time" ASC`);
ORDER BY "Time" ASC`,
true,
);
} else {
onQueryStringChange(`SELECT
onQueryStringChange(
`SELECT
"${nodeData.label}",
COUNT(*) AS "Count"
FROM ${escapeSqlIdentifier(columnTable)}
WHERE "__time" >= CURRENT_TIMESTAMP - INTERVAL '1' DAY
GROUP BY 1
ORDER BY "Count" DESC`);
ORDER BY "Count" DESC`,
true,
);
}
} else {
onQueryStringChange(`SELECT
onQueryStringChange(
`SELECT
${escapeSqlIdentifier(String(nodeData.label))},
COUNT(*) AS "Count"
FROM ${columnSchema}.${columnTable}
GROUP BY 1
ORDER BY "Count" DESC`);
ORDER BY "Count" DESC`,
true,
);
}
}
export interface ColumnTreeProps {
columnMetadataLoading: boolean;
columnMetadata?: ColumnMetadata[];
onQueryStringChange: (queryString: string) => void;
onQueryStringChange: (queryString: string, run: boolean) => void;
defaultSchema?: string;
defaultTable?: string;
addFunctionToGroupBy: (

View File

@ -173,8 +173,14 @@ export class QueryInput extends React.PureComponent<QueryInputProps, QueryInputS
this.setState({ editorHeight: entries[0].contentRect.height });
};
private handleChange = (value: string) => {
// This gets the event as a second arg
const { onQueryStringChange } = this.props;
onQueryStringChange(value);
};
render(): JSX.Element {
const { queryString, runeMode, onQueryStringChange } = this.props;
const { queryString, runeMode } = this.props;
const { editorHeight } = this.state;
// Set the key in the AceEditor to force a rebind and prevent an error that happens otherwise
@ -186,7 +192,7 @@ export class QueryInput extends React.PureComponent<QueryInputProps, QueryInputS
mode={runeMode ? 'hjson' : 'dsql'}
theme="solarized_dark"
name="ace-editor"
onChange={onQueryStringChange}
onChange={this.handleChange}
focus
fontSize={14}
width="100%"

View File

@ -86,7 +86,7 @@ const parser = memoizeOne((sql: string) => {
interface QueryWithContext {
queryString: string;
queryContext: QueryContext;
wrapQuery?: boolean;
wrapQuery: boolean;
}
export interface QueryViewProps {
@ -103,6 +103,8 @@ export interface QueryViewState {
queryString: string;
queryAst: SqlQuery;
queryContext: QueryContext;
wrapQuery: boolean;
autoRun: boolean;
columnMetadataLoading: boolean;
columnMetadata?: ColumnMetadata[];
@ -123,8 +125,6 @@ export interface QueryViewState {
editContextDialogOpen: boolean;
historyDialogOpen: boolean;
queryHistory: QueryRecord[];
autoRun: boolean;
}
interface QueryResult {
@ -139,6 +139,22 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
return query.replace(/;+((?:\s*--[^\n]*)?\s*)$/, '$1');
}
static isExplainQuery(query: string): boolean {
return /EXPLAIN\sPLAN\sFOR/i.test(query);
}
static wrapInLimitIfNeeded(query: string, limit = 1000): string {
query = QueryView.trimSemicolon(query);
if (QueryView.isExplainQuery(query)) return query;
return `SELECT * FROM (${query}\n) LIMIT ${limit}`;
}
static wrapInExplainIfNeeded(query: string): string {
query = QueryView.trimSemicolon(query);
if (QueryView.isExplainQuery(query)) return query;
return `EXPLAIN PLAN FOR (${query}\n)`;
}
static isJsonLike(queryString: string): boolean {
return queryString.trim().startsWith('{');
}
@ -186,10 +202,32 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
}
const queryAst = queryString ? parser(queryString) : undefined;
const localStorageQueryHistory = localStorageGet(LocalStorageKeys.QUERY_HISTORY);
let queryHistory = [];
if (localStorageQueryHistory) {
let possibleQueryHistory: unknown;
try {
possibleQueryHistory = JSON.parse(localStorageQueryHistory);
} catch {}
if (Array.isArray(possibleQueryHistory)) queryHistory = possibleQueryHistory;
}
const localStorageAutoRun = localStorageGet(LocalStorageKeys.AUTO_RUN);
let autoRun = true;
if (localStorageAutoRun) {
let possibleAutoRun: unknown;
try {
possibleAutoRun = JSON.parse(localStorageAutoRun);
} catch {}
if (typeof possibleAutoRun === 'boolean') autoRun = possibleAutoRun;
}
this.state = {
queryString: queryString ? queryString : '',
queryAst,
queryContext: {},
wrapQuery: true,
autoRun,
columnMetadataLoading: false,
@ -200,10 +238,9 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
editContextDialogOpen: false,
historyDialogOpen: false,
queryHistory: [],
autoRun: true,
queryHistory,
};
this.metadataQueryManager = new QueryManager({
processQuery: async () => {
return await queryDruidSql<ColumnMetadata>({
@ -244,8 +281,8 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
jsonQuery = Hjson.parse(queryString);
} else {
const actualQuery = wrapQuery
? `SELECT * FROM (${QueryView.trimSemicolon(queryString)}\n) LIMIT 1000`
: queryString;
? QueryView.wrapInLimitIfNeeded(queryString)
: QueryView.trimSemicolon(queryString);
if (wrapQuery) wrappedLimit = 1000;
@ -314,9 +351,14 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
this.explainQueryManager = new QueryManager({
processQuery: async (queryWithContext: QueryWithContext) => {
const { queryString, queryContext } = queryWithContext;
const { queryString, queryContext, wrapQuery } = queryWithContext;
const actualQuery = wrapQuery
? QueryView.wrapInLimitIfNeeded(queryString)
: QueryView.trimSemicolon(queryString);
const explainPayload: Record<string, any> = {
query: `EXPLAIN PLAN FOR (${QueryView.trimSemicolon(queryString)}\n)`,
query: QueryView.wrapInExplainIfNeeded(actualQuery),
resultFormat: 'object',
};
@ -337,28 +379,6 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
componentDidMount(): void {
this.metadataQueryManager.runQuery(null);
const localStorageQueryHistoy = localStorageGet(LocalStorageKeys.QUERY_HISTORY);
let queryHistory;
if (localStorageQueryHistoy) {
try {
queryHistory = JSON.parse(localStorageQueryHistoy);
} catch {}
if (queryHistory) {
this.setState({ queryHistory });
}
}
const localStorageAutoRun = localStorageGet(LocalStorageKeys.AUTO_RUN);
let autoRun;
if (localStorageAutoRun) {
try {
autoRun = JSON.parse(localStorageAutoRun);
} catch {}
if (typeof autoRun === 'boolean') {
this.setState({ autoRun });
}
}
}
componentWillUnmount(): void {
@ -454,7 +474,9 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
error,
columnMetadata,
autoRun,
wrapQuery,
} = this.state;
const runeMode = QueryView.isJsonLike(queryString);
return (
<SplitterLayout
@ -477,7 +499,9 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
<div className="control-bar">
<RunButton
autoRun={autoRun}
setAutoRun={(autoRun: boolean) => this.setAutoRun(autoRun)}
setAutoRun={this.setAutoRun}
wrapQuery={wrapQuery}
setWrapQuery={this.setWrapQuery}
onEditContext={() => this.setState({ editContextDialogOpen: true })}
runeMode={runeMode}
queryContext={queryContext}
@ -515,31 +539,17 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
preferablyRun: boolean,
alias: Alias,
): void => {
const { autoRun, queryAst } = this.state;
const { queryAst } = this.state;
if (!queryAst) return;
const groupedAst = queryAst.addFunctionToGroupBy(functionName, spacing, argumentsArray, alias);
const queryString = groupedAst.toString();
this.setState({
queryString,
queryAst: parser(queryString),
});
if (autoRun && preferablyRun) {
this.handleRun(true, queryString);
}
const modifiedAst = queryAst.addFunctionToGroupBy(functionName, spacing, argumentsArray, alias);
this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
};
private addToGroupBy = (columnName: string, preferablyRun: boolean): void => {
const { autoRun, queryAst } = this.state;
const { queryAst } = this.state;
if (!queryAst) return;
const groupedAst = queryAst.addToGroupBy(columnName);
const queryString = groupedAst.toString();
this.setState({
queryString,
queryAst: parser(queryString),
});
if (autoRun && preferablyRun) {
this.handleRun(true, queryString);
}
const modifiedAst = queryAst.addToGroupBy(columnName);
this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
};
private addAggregateColumn = (
@ -550,7 +560,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
distinct?: boolean,
filter?: FilterClause,
): void => {
const { autoRun, queryAst } = this.state;
const { queryAst } = this.state;
if (!queryAst) return;
const modifiedAst = queryAst.addAggregateColumn(
columnName,
@ -559,14 +569,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
distinct,
filter,
);
const queryString = modifiedAst.toString();
this.setState({
queryString,
queryAst: parser(queryString),
});
if (autoRun && preferablyRun) {
this.handleRun(true, queryString);
}
this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
};
private sqlOrderBy = (
@ -574,35 +577,21 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
direction: 'ASC' | 'DESC',
preferablyRun: boolean,
): void => {
const { autoRun, queryAst } = this.state;
const { queryAst } = this.state;
if (!queryAst) return;
const modifiedAst = queryAst.orderBy(header, direction);
const queryString = modifiedAst.toString();
this.setState({
queryString,
queryAst: parser(queryString),
});
if (autoRun && preferablyRun) {
this.handleRun(true, queryString);
}
this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
};
private sqlExcludeColumn = (header: string, preferablyRun: boolean): void => {
const { autoRun, queryAst } = this.state;
const { queryAst } = this.state;
if (!queryAst) return;
const modifiedAst = queryAst.excludeColumn(header);
const queryString = modifiedAst.toString();
this.setState({
queryString,
queryAst: parser(queryString),
});
if (autoRun && preferablyRun) {
this.handleRun(true, queryString);
}
this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
};
private sqlFilterRow = (filters: RowFilter[], preferablyRun: boolean): void => {
const { autoRun, queryAst } = this.state;
const { queryAst } = this.state;
if (!queryAst) return;
let modifiedAst: SqlQuery = queryAst;
@ -611,14 +600,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
modifiedAst = modifiedAst.filterRow(filter.header, filter.row, filter.operator);
}
}
const queryString = modifiedAst.toString();
this.setState({
queryString,
queryAst: parser(queryString),
});
if (autoRun && preferablyRun) {
this.handleRun(true, queryString);
}
this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
};
private sqlClearWhere = (): void => {
@ -630,8 +612,11 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
}
};
private handleQueryStringChange = (queryString: string): void => {
this.setState({ queryString, queryAst: parser(queryString) });
private handleQueryStringChange = (queryString: string, preferablyRun?: boolean): void => {
this.setState({ queryString, queryAst: parser(queryString) }, () => {
const { autoRun } = this.state;
if (preferablyRun && autoRun) this.handleRun();
});
};
private handleQueryContextChange = (queryContext: QueryContext) => {
@ -643,18 +628,19 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
localStorageSet(LocalStorageKeys.AUTO_RUN, String(autoRun));
};
private handleRun = (wrapQuery: boolean, customQueryString?: string) => {
const { queryString, queryContext, queryHistory } = this.state;
if (!customQueryString) {
customQueryString = queryString;
}
private setWrapQuery = (wrapQuery: boolean) => {
this.setState({ wrapQuery });
};
private handleRun = () => {
const { queryString, queryContext, wrapQuery, queryHistory } = this.state;
while (queryHistory.length > 9) {
queryHistory.pop();
}
queryHistory.unshift({
version: `${new Date().toISOString()}`,
queryString: customQueryString,
queryString,
});
let queryHistoryString;
try {
@ -664,16 +650,17 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
localStorageSet(LocalStorageKeys.QUERY_HISTORY, queryHistoryString);
}
if (QueryView.isJsonLike(customQueryString) && !QueryView.validRune(customQueryString)) return;
if (QueryView.isJsonLike(queryString) && !QueryView.validRune(queryString)) return;
localStorageSet(LocalStorageKeys.QUERY_KEY, customQueryString);
this.sqlQueryManager.runQuery({ queryString: customQueryString, queryContext, wrapQuery });
localStorageSet(LocalStorageKeys.QUERY_KEY, queryString);
this.sqlQueryManager.runQuery({ queryString, queryContext, wrapQuery });
};
private handleExplain = () => {
const { queryString, queryContext } = this.state;
const { queryString, queryContext, wrapQuery } = this.state;
this.setState({ explainDialogOpen: true });
this.explainQueryManager.runQuery({ queryString, queryContext });
this.explainQueryManager.runQuery({ queryString, queryContext, wrapQuery });
};
private handleSecondaryPaneSizeChange = (secondaryPaneSize: number) => {
@ -702,24 +689,13 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
return queryAst;
};
private onQueryStringChange = (queryString: string) => {
const { autoRun } = this.state;
this.handleQueryStringChange(queryString);
if (autoRun) {
this.handleRun(true, queryString);
}
};
render(): JSX.Element {
const { columnMetadata, columnMetadataLoading, columnMetadataError, queryAst } = this.state;
let defaultSchema;
if (queryAst && queryAst instanceof SqlQuery) {
defaultSchema = queryAst.getSchema();
}
let defaultTable;
if (queryAst && queryAst instanceof SqlQuery) {
if (queryAst instanceof SqlQuery) {
defaultSchema = queryAst.getSchema();
defaultTable = queryAst.getTableName();
}
@ -738,7 +714,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
queryAst={this.getQueryAst}
columnMetadataLoading={columnMetadataLoading}
columnMetadata={columnMetadata}
onQueryStringChange={this.onQueryStringChange}
onQueryStringChange={this.handleQueryStringChange}
defaultSchema={defaultSchema ? defaultSchema : 'druid'}
defaultTable={defaultTable}
/>

View File

@ -26,7 +26,9 @@ describe('run button', () => {
const runButton = (
<RunButton
autoRun
setAutoRun={() => null}
setAutoRun={() => {}}
wrapQuery
setWrapQuery={() => {}}
onHistory={() => null}
onEditContext={() => null}
runeMode={false}

View File

@ -46,21 +46,19 @@ import { DRUID_DOCS_RUNE, DRUID_DOCS_SQL } from '../../../variables';
export interface RunButtonProps {
runeMode: boolean;
autoRun: boolean;
setAutoRun: (autoRun: boolean) => void;
wrapQuery: boolean;
setWrapQuery: (wrapQuery: boolean) => void;
queryContext: QueryContext;
onQueryContextChange: (newQueryContext: QueryContext) => void;
onRun: (wrapQuery: boolean) => void;
onRun: () => void;
onExplain: () => void;
onEditContext: () => void;
onHistory: () => void;
setAutoRun: (autoRun: boolean) => void;
}
interface RunButtonState {
wrapQuery: boolean;
}
@HotkeysTarget
export class RunButton extends React.PureComponent<RunButtonProps, RunButtonState> {
export class RunButton extends React.PureComponent<RunButtonProps> {
constructor(props: RunButtonProps, context: any) {
super(props, context);
this.state = {
@ -85,7 +83,7 @@ export class RunButton extends React.PureComponent<RunButtonProps, RunButtonStat
private handleRun = () => {
const { onRun } = this.props;
if (!onRun) return;
onRun(this.state.wrapQuery);
onRun();
};
renderExtraMenu() {
@ -96,10 +94,11 @@ export class RunButton extends React.PureComponent<RunButtonProps, RunButtonStat
onQueryContextChange,
onEditContext,
onHistory,
setAutoRun,
autoRun,
setAutoRun,
wrapQuery,
setWrapQuery,
} = this.props;
const { wrapQuery } = this.state;
const useCache = getUseCache(queryContext);
const useApproximateCountDistinct = getUseApproximateCountDistinct(queryContext);
@ -120,7 +119,7 @@ export class RunButton extends React.PureComponent<RunButtonProps, RunButtonStat
<MenuCheckbox
checked={wrapQuery}
label="Wrap query with limit"
onChange={() => this.setState({ wrapQuery: !wrapQuery })}
onChange={() => setWrapQuery(!wrapQuery)}
/>
<MenuCheckbox
checked={autoRun}
@ -160,8 +159,7 @@ export class RunButton extends React.PureComponent<RunButtonProps, RunButtonStat
}
render(): JSX.Element {
const { runeMode, onRun } = this.props;
const { wrapQuery } = this.state;
const { runeMode, onRun, wrapQuery } = this.props;
return (
<ButtonGroup className="run-button">