Web console: more explicit limit on run button (#8378)

* update sql doc parsing

* keyword fixes

* fix header default

* tidy

* fix tests
This commit is contained in:
Vadim Ogievetsky 2019-08-23 13:40:48 -07:00 committed by Clint Wylie
parent 368ace4e87
commit 20ea90a5a6
26 changed files with 317 additions and 247 deletions

View File

@ -10,7 +10,7 @@ coordinator-console/
pages/
index.html
lib/sql-function-doc.ts
lib/sql-function-doc.js
.tscache
tscommand-*.tmp.txt

22
web-console/lib/keywords.d.ts vendored Normal file
View File

@ -0,0 +1,22 @@
/*
* 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.
*/
export const SQL_KEYWORDS: string[];
export const SQL_EXPRESSION_PARTS: string[];
export const SQL_CONSTANTS: string[];
export const SQL_DYNAMICS: string[];

View File

@ -18,7 +18,7 @@
// Hand picked from https://druid.apache.org/docs/latest/querying/sql.html
export const SQL_KEYWORDS: string[] = [
exports.SQL_KEYWORDS = [
'EXPLAIN PLAN FOR',
'WITH',
'AS',
@ -36,7 +36,7 @@ export const SQL_KEYWORDS: string[] = [
'UNION ALL',
];
export const SQL_EXPRESSION_PARTS: string[] = [
exports.SQL_EXPRESSION_PARTS = [
'FILTER',
'END',
'ELSE',
@ -69,6 +69,6 @@ export const SQL_EXPRESSION_PARTS: string[] = [
'INTERVAL',
];
export const SQL_CONSTANTS: string[] = ['NULL', 'FALSE', 'TRUE'];
exports.SQL_CONSTANTS = ['NULL', 'FALSE', 'TRUE'];
export const SQL_DYNAMICS: string[] = ['CURRENT_TIMESTAMP', 'CURRENT_DATE'];
exports.SQL_DYNAMICS = ['CURRENT_TIMESTAMP', 'CURRENT_DATE'];

29
web-console/lib/sql-function-doc.d.ts vendored Normal file
View File

@ -0,0 +1,29 @@
/*
* 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.
*/
export interface SyntaxDescription {
name: string;
description: string;
}
export interface FunctionSyntaxDescription extends SyntaxDescription {
arguments: string;
}
export const SQL_DATA_TYPES: SyntaxDescription[];
export const SQL_FUNCTIONS: FunctionSyntaxDescription[];

View File

@ -4428,9 +4428,9 @@
"integrity": "sha512-0sYnfUHHMoajaud/i5BHKA12bUxiWEHJ9rxGqVEppFxsEcxef0TZQ5J59lU+UniEBcz/sG5fTESRyS7cOm3tSQ=="
},
"druid-query-toolkit": {
"version": "0.3.23",
"resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.3.23.tgz",
"integrity": "sha512-6wVAGFw1sjLT9U5f7QNaIKCS0VgUfWLn/X8YWf3YQN3awSCxClzFUQnLKnHeIEb7ot0ca4H6axlb7NpzGqmtzA==",
"version": "0.3.24",
"resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.3.24.tgz",
"integrity": "sha512-kFvEXAjjNuJYpeRsAzzO/cJ2rr4nHBGTSCAA4UPxyt4pKNZE/OUap7IQbsdnxYmhkHgfjUBGcFteufaVHSn7SA==",
"requires": {
"tslib": "^1.10.0"
}

View File

@ -61,7 +61,7 @@
"d3": "^5.10.1",
"d3-array": "^2.3.1",
"druid-console": "0.0.2",
"druid-query-toolkit": "^0.3.23",
"druid-query-toolkit": "^0.3.24",
"file-saver": "^2.0.2",
"has-own-prop": "^2.0.0",
"hjson": "^3.1.2",

View File

@ -17,8 +17,8 @@
# limitations under the License.
rm -rf \
lib/*.css \
lib/*.ts \
lib/react-table.css \
lib/sql-function-doc.js \
node_modules \
coordinator-console \
pages \

View File

@ -21,7 +21,7 @@
const fs = require('fs-extra');
const readfile = '../docs/querying/sql.md';
const writefile = 'lib/sql-function-doc.ts';
const writefile = 'lib/sql-function-doc.js';
const readDoc = async () => {
const data = await fs.readFile(readfile, 'utf-8');
@ -30,18 +30,19 @@ const readDoc = async () => {
const functionDocs = [];
const dataTypeDocs = [];
for (let line of lines) {
const functionMatch = line.match(/^\|`(.+\(.*\))`\|(.+)\|$/);
const functionMatch = line.match(/^\|`(\w+)(\(.*\))`\|(.+)\|$/);
if (functionMatch) {
functionDocs.push({
syntax: functionMatch[1],
description: functionMatch[2],
name: functionMatch[1],
arguments: functionMatch[2],
description: functionMatch[3],
});
}
const dataTypeMatch = line.match(/^\|([A-Z]+)\|([A-Z]+)\|(.*)\|(.*)\|$/);
if (dataTypeMatch) {
dataTypeDocs.push({
syntax: dataTypeMatch[1],
name: dataTypeMatch[1],
description: dataTypeMatch[4] || `Druid runtime type: ${dataTypeMatch[2]}`,
});
}
@ -81,16 +82,11 @@ const readDoc = async () => {
// This file is auto generated and should not be modified
export interface SyntaxDescription {
syntax: string;
description: string;
}
// prettier-ignore
exports.SQL_DATA_TYPES = ${JSON.stringify(dataTypeDocs, null, 2)};
// prettier-ignore
export const SQL_FUNCTIONS: SyntaxDescription[] = ${JSON.stringify(functionDocs, null, 2)};
// prettier-ignore
export const SQL_DATE_TYPES: SyntaxDescription[] = ${JSON.stringify(dataTypeDocs, null, 2)};
exports.SQL_FUNCTIONS = ${JSON.stringify(functionDocs, null, 2)};
`;
await fs.writeFile(writefile, content, 'utf-8');

View File

@ -21,6 +21,9 @@
// Originally licensed under the MIT license (https://github.com/thlorenz/brace/blob/master/LICENSE)
// This file was modified to make the list of keywords more closely adhere to what is found in DruidSQL
var druidKeywords = require('../../lib/keywords');
var druidFunctions = require('../../lib/sql-function-doc');
ace.define(
'ace/mode/dsql_highlight_rules',
['require', 'exports', 'module', 'ace/lib/oop', 'ace/mode/text_highlight_rules'],
@ -31,20 +34,25 @@ ace.define(
var TextHighlightRules = acequire('./text_highlight_rules').TextHighlightRules;
var SqlHighlightRules = function() {
var keywords =
'select|from|where|and|or|group|by|order|limit|offset|having|as|case|' +
'when|else|end|type|on|desc|asc|union|create|table|if|' +
'foreign|not|references|default|null|inner|cross|drop|grant';
// Stuff like: 'with|select|from|where|and|or|group|by|order|limit|having|as|case|'
var keywords = druidKeywords.SQL_KEYWORDS.concat(druidKeywords.SQL_EXPRESSION_PARTS)
.join('|')
.replace(/\s/g, '|');
var builtinConstants = 'true|false';
// Stuff like: 'true|false'
var builtinConstants = druidKeywords.SQL_CONSTANTS.join('|');
var builtinFunctions =
'avg|count|first|last|max|min|sum|ucase|lcase|mid|len|round|rank|now|format|' +
'coalesce|ifnull|isnull|nvl';
// Stuff like: 'avg|count|first|last|max|min'
var builtinFunctions = druidKeywords.SQL_DYNAMICS.concat(
druidFunctions.SQL_FUNCTIONS.map(function(f) {
return f.name;
}),
).join('|');
var dataTypes =
'int|numeric|decimal|date|varchar|char|bigint|float|double|bit|binary|text|set|timestamp|' +
'money|real|number|integer';
// Stuff like: 'int|numeric|decimal|date|varchar|char|bigint|float|double|bit|binary|text|set|timestamp'
var dataTypes = druidFunctions.SQL_DATA_TYPES.map(function(f) {
return f.name;
}).join('|');
var keywordMapper = this.createKeywordMapper(
{

View File

@ -36,8 +36,6 @@ function removeFirstPartialLine(log: string): string {
return lines.join('\n');
}
let interval: number | undefined;
export interface ShowLogProps {
endpoint: string;
downloadFilename?: string;
@ -53,8 +51,11 @@ export interface ShowLogState {
}
export class ShowLog extends React.PureComponent<ShowLogProps, ShowLogState> {
static CHECK_INTERVAL = 2500;
private showLogQueryManager: QueryManager<null, string>;
public log = React.createRef<HTMLTextAreaElement>();
private log = React.createRef<HTMLTextAreaElement>();
private interval: number | undefined;
constructor(props: ShowLogProps, context: any) {
super(props, context);
@ -87,24 +88,40 @@ export class ShowLog extends React.PureComponent<ShowLogProps, ShowLogState> {
const { status } = this.props;
if (status === 'RUNNING') {
interval = Number(setInterval(() => this.showLogQueryManager.runQuery(null), 2000));
this.addTailer();
}
this.showLogQueryManager.runQuery(null);
}
componentWillUnmount(): void {
if (interval) clearInterval(interval);
this.removeTailer();
}
addTailer() {
if (this.interval) return;
this.interval = Number(
setInterval(() => this.showLogQueryManager.runQuery(null), ShowLog.CHECK_INTERVAL),
);
}
removeTailer() {
if (!this.interval) return;
clearInterval(this.interval);
delete this.interval;
}
private handleCheckboxChange = () => {
const { tail } = this.state;
const nextTail = !tail;
this.setState({
tail: !this.state.tail,
tail: nextTail,
});
if (!this.state.tail) {
interval = Number(setInterval(() => this.showLogQueryManager.runQuery(null), 2000));
if (nextTail) {
this.addTailer();
} else {
if (interval) clearInterval(interval);
this.removeTailer();
}
};

View File

@ -75,10 +75,11 @@ export class QueryHistoryDialog extends React.PureComponent<
}
private handleSelect = () => {
const { queryRecords, setQueryString } = this.props;
const { queryRecords, setQueryString, onClose } = this.props;
const { activeTab } = this.state;
setQueryString(queryRecords[activeTab].queryString);
onClose();
};
private handleTabChange = (tab: number) => {

View File

@ -148,7 +148,10 @@ export class QueryPlanDialog extends React.PureComponent<
<Button
text="Open"
intent={Intent.PRIMARY}
onClick={() => setQueryString(this.queryString)}
onClick={() => {
setQueryString(this.queryString);
onClose();
}}
/>
</div>
</div>

View File

@ -30,6 +30,8 @@ import {
} from './druid-time';
import { deepGet, deepSet } from './object-change';
export const MAX_INLINE_DATA_LENGTH = 65536;
// These constants are used to make sure that they are not constantly recreated thrashing the pure components
export const EMPTY_OBJECT: any = {};
export const EMPTY_ARRAY: any[] = [];
@ -290,7 +292,7 @@ const PARSE_SPEC_FORM_FIELDS: Field<ParseSpec>[] = [
{
name: 'hasHeaderRow',
type: 'boolean',
defaultValue: true,
defaultValue: false,
defined: (p: ParseSpec) => p.format === 'csv' || p.format === 'tsv',
},
{

View File

@ -105,6 +105,7 @@ import {
issueWithIoConfig,
issueWithParser,
joinFilter,
MAX_INLINE_DATA_LENGTH,
MetricSpec,
normalizeSpec,
Parser,
@ -947,7 +948,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
placeholder="Paste your data here"
value={deepGet(spec, 'ioConfig.firehose.data')}
onChange={(e: any) => {
const stringValue = e.target.value.substr(0, 65536);
const stringValue = e.target.value.substr(0, MAX_INLINE_DATA_LENGTH);
this.updateSpec(deepSet(spec, 'ioConfig.firehose.data', stringValue));
}}
/>

View File

@ -10,10 +10,8 @@ exports[`sql view matches snapshot 1`] = `
addToGroupBy={[Function]}
clear={[Function]}
columnMetadataLoading={true}
currentFilters={[Function]}
defaultSchema="druid"
filterByRow={[Function]}
hasGroupBy={[Function]}
onQueryStringChange={[Function]}
queryAst={[Function]}
replaceFrom={[Function]}
@ -44,6 +42,7 @@ exports[`sql view matches snapshot 1`] = `
>
<HotkeysTarget(RunButton)
autoRun={true}
onAutoRunChange={[Function]}
onEditContext={[Function]}
onExplain={[Function]}
onHistory={[Function]}
@ -51,10 +50,20 @@ exports[`sql view matches snapshot 1`] = `
onRun={[Function]}
queryContext={Object {}}
runeMode={false}
setAutoRun={[Function]}
setWrapQuery={[Function]}
wrapQuery={true}
/>
<Blueprint3.Tooltip
content="Automatically wrap the query with a limit to protect against queries with very large result sets"
hoverCloseDelay={0}
hoverOpenDelay={800}
transitionDuration={100}
>
<Blueprint3.Switch
checked={true}
className="smart-query-limit"
label="Smart query limit"
onChange={[Function]}
/>
</Blueprint3.Tooltip>
</div>
</div>
<QueryOutput

View File

@ -27,9 +27,7 @@ describe('column tree', () => {
it('matches snapshot', () => {
const columnTree = (
<ColumnTree
currentFilters={() => []}
queryAst={() => undefined}
hasGroupBy={() => false}
clear={() => null}
addFunctionToGroupBy={() => null}
filterByRow={() => null}

View File

@ -126,7 +126,6 @@ export interface ColumnTreeProps {
onQueryStringChange: (queryString: string, run: boolean) => void;
defaultSchema?: string;
defaultTable?: string;
currentFilters: () => string[];
addFunctionToGroupBy: (
functionName: string,
spacing: string[],
@ -145,7 +144,6 @@ export interface ColumnTreeProps {
) => void;
filterByRow: (filters: RowFilter[], preferablyRun: boolean) => void;
replaceFrom: (table: RefExpression, preferablyRun: boolean) => void;
hasGroupBy: () => boolean;
queryAst: () => SqlQuery | undefined;
clear: (column: string, preferablyRun: boolean) => void;
}
@ -160,6 +158,7 @@ export interface ColumnTreeState {
export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeState> {
static getDerivedStateFromProps(props: ColumnTreeProps, state: ColumnTreeState) {
const { columnMetadata, defaultSchema, defaultTable } = props;
if (columnMetadata && columnMetadata !== state.prevColumnMetadata) {
const columnTree = groupBy(
columnMetadata,
@ -241,7 +240,13 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
targetClassName={'bp3-popover-open'}
content={
<Deferred
content={() => (
content={() => {
const queryAst = props.queryAst();
const hasFilter = queryAst
? queryAst.getCurrentFilters().includes(columnData.COLUMN_NAME)
: false;
return (
<Menu>
<MenuItem
icon={IconNames.FULLSCREEN}
@ -268,10 +273,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
columnName={columnData.COLUMN_NAME}
queryAst={props.queryAst()}
clear={props.clear}
hasFilter={
props.currentFilters() &&
props.currentFilters().includes(columnData.COLUMN_NAME)
}
hasFilter={hasFilter}
/>
)}
{columnData.DATA_TYPE === 'VARCHAR' && (
@ -283,10 +285,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
columnName={columnData.COLUMN_NAME}
queryAst={props.queryAst()}
clear={props.clear}
hasFilter={
props.currentFilters() &&
props.currentFilters().includes(columnData.COLUMN_NAME)
}
hasFilter={hasFilter}
/>
)}
{columnData.DATA_TYPE === 'TIMESTAMP' && (
@ -298,10 +297,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
filterByRow={props.filterByRow}
columnName={columnData.COLUMN_NAME}
queryAst={props.queryAst()}
hasFilter={
props.currentFilters() &&
props.currentFilters().includes(columnData.COLUMN_NAME)
}
hasFilter={hasFilter}
/>
)}
<MenuItem
@ -315,7 +311,8 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
}}
/>
</Menu>
)}
);
}}
/>
}
>
@ -361,7 +358,6 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
columnTree,
selectedTreeIndex,
currentSchemaSubtree,
prevGroupByStatus: props.hasGroupBy,
};
}
return null;

View File

@ -17,7 +17,7 @@ exports[`query extra info matches snapshot 1`] = `
class=""
tabindex="0"
>
1999+ results in 8.00s
999+ results in 8.00s
</span>
</span>
</span>

View File

@ -29,8 +29,8 @@ describe('query extra info', () => {
queryId: 'e3ee781b-c0b6-4385-9d99-a8a1994bebac',
startTime: new Date('1986-04-26T01:23:40+03:00'),
endTime: new Date('1986-04-26T01:23:48+03:00'),
numResults: 2000,
wrappedLimit: 2000,
numResults: 1000,
wrapQueryLimit: 1000,
}}
onDownload={() => {}}
/>

View File

@ -41,7 +41,7 @@ export interface QueryExtraInfoData {
startTime: Date;
endTime: Date;
numResults: number;
wrappedLimit?: number;
wrapQueryLimit: number | undefined;
}
export interface QueryExtraInfoProps {
@ -63,7 +63,10 @@ export class QueryExtraInfo extends React.PureComponent<QueryExtraInfoProps> {
);
let resultCount: string;
if (queryExtraInfo.wrappedLimit && queryExtraInfo.numResults === queryExtraInfo.wrappedLimit) {
if (
queryExtraInfo.wrapQueryLimit &&
queryExtraInfo.numResults === queryExtraInfo.wrapQueryLimit
) {
resultCount = `${queryExtraInfo.numResults - 1}+ results`;
} else {
resultCount = pluralIfNeeded(queryExtraInfo.numResults, 'result');

View File

@ -22,12 +22,16 @@ import escape from 'lodash.escape';
import React from 'react';
import AceEditor from 'react-ace';
import { SQL_DATE_TYPES, SQL_FUNCTIONS, SyntaxDescription } from '../../../../lib/sql-function-doc';
import {
SQL_CONSTANTS,
SQL_DYNAMICS,
SQL_EXPRESSION_PARTS,
SQL_KEYWORDS,
} from '../../../../lib/keywords';
import { SQL_DATA_TYPES, SQL_FUNCTIONS } from '../../../../lib/sql-function-doc';
import { uniq } from '../../../utils';
import { ColumnMetadata } from '../../../utils/column-metadata';
import { SQL_CONSTANTS, SQL_DYNAMICS, SQL_EXPRESSION_PARTS, SQL_KEYWORDS } from './keywords';
import './query-input.scss';
const langTools = ace.acequire('ace/ext/language_tools');
@ -60,7 +64,7 @@ export class QueryInput extends React.PureComponent<QueryInputProps, QueryInputS
SQL_EXPRESSION_PARTS.map(v => ({ name: v, value: v, score: 0, meta: 'keyword' })),
SQL_CONSTANTS.map(v => ({ name: v, value: v, score: 0, meta: 'constant' })),
SQL_DYNAMICS.map(v => ({ name: v, value: v, score: 0, meta: 'dynamic' })),
SQL_DATE_TYPES.map(v => ({ name: v.syntax, value: v.syntax, score: 0, meta: 'keyword' })),
SQL_DATA_TYPES.map(v => ({ name: v.name, value: v.name, score: 0, meta: 'type' })),
);
const keywordCompleter = {
@ -79,13 +83,12 @@ export class QueryInput extends React.PureComponent<QueryInputProps, QueryInputS
static addFunctionAutoCompleter(): void {
if (!langTools) return;
const functionList: any[] = SQL_FUNCTIONS.map((entry: SyntaxDescription) => {
const funcName: string = entry.syntax.replace(/\(.*\)/, '()');
const functionList: any[] = SQL_FUNCTIONS.map(entry => {
return {
value: funcName,
value: entry.name,
score: 80,
meta: 'function',
syntax: entry.syntax,
syntax: entry.name + entry.arguments,
description: entry.description,
completer: {
insertMatch: (editor: any, data: any) => {

View File

@ -58,6 +58,16 @@ $nav-width: 250px;
bottom: 0;
width: 100%;
& > * {
vertical-align: bottom;
margin-right: 15px;
}
.smart-query-limit {
display: inline-block;
margin-bottom: 8px;
}
.query-extra-info {
position: absolute;
right: 0;

View File

@ -16,7 +16,7 @@
* limitations under the License.
*/
import { Intent } from '@blueprintjs/core';
import { Intent, Switch, Tooltip } from '@blueprintjs/core';
import axios from 'axios';
import classNames from 'classnames';
import {
@ -38,7 +38,7 @@ import memoizeOne from 'memoize-one';
import React from 'react';
import SplitterLayout from 'react-splitter-layout';
import { SQL_FUNCTIONS, SyntaxDescription } from '../../../lib/sql-function-doc';
import { SQL_FUNCTIONS } from '../../../lib/sql-function-doc';
import { QueryPlanDialog } from '../../dialogs';
import { EditContextDialog } from '../../dialogs/edit-context-dialog/edit-context-dialog';
import {
@ -69,11 +69,7 @@ import { RunButton } from './run-button/run-button';
import './query-view.scss';
const parserRaw = sqlParserFactory(
SQL_FUNCTIONS.map((sqlFunction: SyntaxDescription) => {
return sqlFunction.syntax.substr(0, sqlFunction.syntax.indexOf('('));
}),
);
const parserRaw = sqlParserFactory(SQL_FUNCTIONS.map(sqlFunction => sqlFunction.name));
const parser = memoizeOne((sql: string) => {
try {
@ -86,7 +82,7 @@ const parser = memoizeOne((sql: string) => {
interface QueryWithContext {
queryString: string;
queryContext: QueryContext;
wrapQuery: boolean;
wrapQueryLimit: number | undefined;
}
export interface QueryViewProps {
@ -103,7 +99,7 @@ export interface QueryViewState {
queryString: string;
queryAst: SqlQuery;
queryContext: QueryContext;
wrapQuery: boolean;
wrapQueryLimit: number | undefined;
autoRun: boolean;
columnMetadataLoading: boolean;
@ -147,8 +143,9 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
return /EXPLAIN\sPLAN\sFOR/i.test(query);
}
static wrapInLimitIfNeeded(query: string, limit = 1000): string {
static wrapInLimitIfNeeded(query: string, limit: number | undefined): string {
query = QueryView.trimSemicolon(query);
if (!limit) return query;
if (QueryView.isExplainQuery(query)) return query;
return `SELECT * FROM (${query}\n) LIMIT ${limit}`;
}
@ -198,12 +195,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
constructor(props: QueryViewProps, context: any) {
super(props, context);
let queryString: string | undefined;
if (props.initQuery) {
queryString = props.initQuery;
} else if (localStorageGet(LocalStorageKeys.QUERY_KEY)) {
queryString = localStorageGet(LocalStorageKeys.QUERY_KEY);
}
const queryString = props.initQuery || localStorageGet(LocalStorageKeys.QUERY_KEY) || '';
const queryAst = queryString ? parser(queryString) : undefined;
const localStorageQueryHistory = localStorageGet(LocalStorageKeys.QUERY_HISTORY);
@ -227,10 +219,10 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
}
this.state = {
queryString: queryString ? queryString : '',
queryString,
queryAst,
queryContext: {},
wrapQuery: true,
wrapQueryLimit: 100,
autoRun,
columnMetadataLoading: false,
@ -268,27 +260,22 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
this.sqlQueryManager = new QueryManager({
processQuery: async (queryWithContext: QueryWithContext): Promise<QueryResult> => {
const { queryString, queryContext, wrapQuery } = queryWithContext;
const { queryString, queryContext, wrapQueryLimit } = queryWithContext;
let ast: SqlQuery | undefined;
let wrappedLimit: number | undefined;
let parsedQuery: SqlQuery | undefined;
let jsonQuery: any;
try {
ast = parser(queryString);
parsedQuery = parser(queryString);
} catch {}
if (!(ast instanceof SqlQuery)) {
ast = undefined;
if (!(parsedQuery instanceof SqlQuery)) {
parsedQuery = undefined;
}
if (QueryView.isJsonLike(queryString)) {
jsonQuery = Hjson.parse(queryString);
} else {
const actualQuery = wrapQuery
? QueryView.wrapInLimitIfNeeded(queryString)
: QueryView.trimSemicolon(queryString);
if (wrapQuery) wrappedLimit = 1000;
const actualQuery = QueryView.wrapInLimitIfNeeded(queryString, wrapQueryLimit);
jsonQuery = {
query: actualQuery,
@ -339,9 +326,9 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
startTime,
endTime,
numResults: queryResult.rows.length,
wrappedLimit,
wrapQueryLimit,
},
parsedQuery: ast,
parsedQuery,
};
},
onStateChange: ({ result, loading, error }) => {
@ -355,11 +342,9 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
this.explainQueryManager = new QueryManager({
processQuery: async (queryWithContext: QueryWithContext) => {
const { queryString, queryContext, wrapQuery } = queryWithContext;
const { queryString, queryContext, wrapQueryLimit } = queryWithContext;
const actualQuery = wrapQuery
? QueryView.wrapInLimitIfNeeded(queryString)
: QueryView.trimSemicolon(queryString);
const actualQuery = QueryView.wrapInLimitIfNeeded(queryString, wrapQueryLimit);
const explainPayload: Record<string, any> = {
query: QueryView.wrapInExplainIfNeeded(actualQuery),
@ -431,10 +416,8 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
<QueryPlanDialog
explainResult={explainResult}
explainError={explainError}
setQueryString={this.handleQueryStringChange}
onClose={() => this.setState({ explainDialogOpen: false })}
setQueryString={(queryString: string) =>
this.setState({ queryString, explainDialogOpen: false, queryAst: parser(queryString) })
}
/>
);
}
@ -446,9 +429,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
return (
<QueryHistoryDialog
queryRecords={queryHistory}
setQueryString={queryString =>
this.setState({ queryString, queryAst: parser(queryString), historyDialogOpen: false })
}
setQueryString={this.handleQueryStringChange}
onClose={() => this.setState({ historyDialogOpen: false })}
/>
);
@ -469,6 +450,25 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
);
}
renderWrapQueryLimitSelector() {
const { wrapQueryLimit, queryString } = this.state;
if (QueryView.isJsonLike(queryString)) return;
return (
<Tooltip
content="Automatically wrap the query with a limit to protect against queries with very large result sets"
hoverOpenDelay={800}
>
<Switch
className="smart-query-limit"
checked={Boolean(wrapQueryLimit)}
label="Smart query limit"
onChange={() => this.handleWrapQueryLimitChange(wrapQueryLimit ? undefined : 100)}
/>
</Tooltip>
);
}
renderMainArea() {
const {
queryString,
@ -478,7 +478,6 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
error,
columnMetadata,
autoRun,
wrapQuery,
} = this.state;
const emptyQuery = QueryView.isEmptyQuery(queryString);
@ -521,9 +520,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
<div className="control-bar">
<RunButton
autoRun={autoRun}
setAutoRun={this.setAutoRun}
wrapQuery={wrapQuery}
setWrapQuery={this.setWrapQuery}
onAutoRunChange={this.handleAutoRunChange}
onEditContext={() => this.setState({ editContextDialogOpen: true })}
runeMode={runeMode}
queryContext={queryContext}
@ -532,6 +529,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
onExplain={emptyQuery ? undefined : this.handleExplain}
onHistory={() => this.setState({ historyDialogOpen: true })}
/>
{this.renderWrapQueryLimitSelector()}
{result && (
<QueryExtraInfo
queryExtraInfo={result.queryExtraInfo}
@ -563,6 +561,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
): void => {
const { queryAst } = this.state;
if (!queryAst) return;
const modifiedAst = queryAst.addFunctionToGroupBy(functionName, spacing, argumentsArray, alias);
this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
};
@ -570,6 +569,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
private addToGroupBy = (columnName: string, preferablyRun: boolean): void => {
const { queryAst } = this.state;
if (!queryAst) return;
const modifiedAst = queryAst.addToGroupBy(columnName);
this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
};
@ -577,6 +577,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
private replaceFrom = (table: RefExpression, preferablyRun: boolean): void => {
const { queryAst } = this.state;
if (!queryAst) return;
const modifiedAst = queryAst.replaceFrom(table);
this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
};
@ -591,6 +592,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
): void => {
const { queryAst } = this.state;
if (!queryAst) return;
const modifiedAst = queryAst.addAggregateColumn(
columnName,
functionName,
@ -608,6 +610,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
): void => {
const { queryAst } = this.state;
if (!queryAst) return;
const modifiedAst = queryAst.orderBy(header, direction);
this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
};
@ -615,6 +618,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
private sqlExcludeColumn = (header: string, preferablyRun: boolean): void => {
const { queryAst } = this.state;
if (!queryAst) return;
const modifiedAst = queryAst.excludeColumn(header);
this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
};
@ -648,17 +652,17 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
this.setState({ queryContext });
};
private setAutoRun = (autoRun: boolean) => {
private handleAutoRunChange = (autoRun: boolean) => {
this.setState({ autoRun });
localStorageSet(LocalStorageKeys.AUTO_RUN, String(autoRun));
};
private setWrapQuery = (wrapQuery: boolean) => {
this.setState({ wrapQuery });
private handleWrapQueryLimitChange = (wrapQueryLimit: number | undefined) => {
this.setState({ wrapQueryLimit });
};
private handleRun = () => {
const { queryString, queryContext, wrapQuery, queryHistory } = this.state;
const { queryString, queryContext, wrapQueryLimit, queryHistory } = this.state;
if (QueryView.isJsonLike(queryString) && !QueryView.validRune(queryString)) return;
const newQueryHistory = QueryHistoryDialog.addQueryToHistory(queryHistory, queryString);
@ -667,50 +671,25 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
localStorageSet(LocalStorageKeys.QUERY_KEY, queryString);
this.setState({ queryHistory: newQueryHistory });
this.sqlQueryManager.runQuery({ queryString, queryContext, wrapQuery });
this.sqlQueryManager.runQuery({ queryString, queryContext, wrapQueryLimit });
};
private handleExplain = () => {
const { queryString, queryContext, wrapQuery } = this.state;
const { queryString, queryContext, wrapQueryLimit } = this.state;
this.setState({ explainDialogOpen: true });
this.explainQueryManager.runQuery({ queryString, queryContext, wrapQuery });
this.explainQueryManager.runQuery({ queryString, queryContext, wrapQueryLimit });
};
private handleSecondaryPaneSizeChange = (secondaryPaneSize: number) => {
localStorageSet(LocalStorageKeys.QUERY_VIEW_PANE_SIZE, String(secondaryPaneSize));
};
private getGroupBySetting = () => {
const { queryString, queryAst } = this.state;
const ast = queryAst;
let tempAst: SqlQuery | undefined;
if (!ast) {
tempAst = parser(queryString);
}
let hasGroupBy = false;
if (ast && ast instanceof SqlQuery) {
hasGroupBy = !!ast.groupByClause;
} else if (tempAst && tempAst instanceof SqlQuery) {
hasGroupBy = !!tempAst.groupByClause;
}
return hasGroupBy;
};
private getQueryAst = () => {
const { queryAst } = this.state;
return queryAst;
};
private getCurrentFilters = () => {
const { queryAst } = this.state;
if (queryAst) {
return queryAst.getCurrentFilters();
}
return [];
};
render(): JSX.Element {
const { columnMetadata, columnMetadataLoading, columnMetadataError, queryAst } = this.state;
@ -732,14 +711,12 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
addFunctionToGroupBy={this.addFunctionToGroupBy}
addAggregateColumn={this.addAggregateColumn}
addToGroupBy={this.addToGroupBy}
hasGroupBy={this.getGroupBySetting}
queryAst={this.getQueryAst}
columnMetadataLoading={columnMetadataLoading}
columnMetadata={columnMetadata}
onQueryStringChange={this.handleQueryStringChange}
defaultSchema={defaultSchema ? defaultSchema : 'druid'}
defaultTable={defaultTable}
currentFilters={this.getCurrentFilters}
replaceFrom={this.replaceFrom}
/>
)}

View File

@ -11,7 +11,7 @@ exports[`run button matches snapshot 1`] = `
class="bp3-popover-target"
>
<button
class="bp3-button"
class="bp3-button bp3-intent-primary"
tabindex="0"
type="button"
>
@ -37,7 +37,7 @@ exports[`run button matches snapshot 1`] = `
<span
class="bp3-button-text"
>
Run with limit
Run
</span>
</button>
</span>
@ -49,7 +49,7 @@ exports[`run button matches snapshot 1`] = `
class="bp3-popover-target"
>
<button
class="bp3-button"
class="bp3-button bp3-intent-primary"
type="button"
>
<span

View File

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

View File

@ -22,6 +22,7 @@ import {
Hotkey,
Hotkeys,
HotkeysTarget,
Intent,
Menu,
MenuItem,
Popover,
@ -46,9 +47,7 @@ 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;
onAutoRunChange: (autoRun: boolean) => void;
queryContext: QueryContext;
onQueryContextChange: (newQueryContext: QueryContext) => void;
onRun: (() => void) | undefined;
@ -92,9 +91,7 @@ export class RunButton extends React.PureComponent<RunButtonProps> {
onEditContext,
onHistory,
autoRun,
setAutoRun,
wrapQuery,
setWrapQuery,
onAutoRunChange,
} = this.props;
const useCache = getUseCache(queryContext);
@ -113,15 +110,10 @@ export class RunButton extends React.PureComponent<RunButtonProps> {
<>
{onExplain && <MenuItem icon={IconNames.CLEAN} text="Explain" onClick={onExplain} />}
<MenuItem icon={IconNames.HISTORY} text="History" onClick={onHistory} />
<MenuCheckbox
checked={wrapQuery}
label="Wrap query with limit"
onChange={() => setWrapQuery(!wrapQuery)}
/>
<MenuCheckbox
checked={autoRun}
label="Auto run queries"
onChange={() => setAutoRun(!autoRun)}
onChange={() => onAutoRunChange(!autoRun)}
/>
<MenuCheckbox
checked={useApproximateCountDistinct}
@ -156,20 +148,25 @@ export class RunButton extends React.PureComponent<RunButtonProps> {
}
render(): JSX.Element {
const { runeMode, onRun, wrapQuery } = this.props;
const runButtonText = runeMode ? 'Rune' : wrapQuery ? 'Run with limit' : 'Run as is';
const { runeMode, onRun } = this.props;
const runButtonText = 'Run' + (runeMode ? 'e' : '');
return (
<ButtonGroup className="run-button">
{onRun ? (
<Tooltip content="Control + Enter" hoverOpenDelay={900}>
<Button icon={IconNames.CARET_RIGHT} onClick={this.handleRun} text={runButtonText} />
<Button
icon={IconNames.CARET_RIGHT}
onClick={this.handleRun}
text={runButtonText}
intent={Intent.PRIMARY}
/>
</Tooltip>
) : (
<Button icon={IconNames.CARET_RIGHT} text={runButtonText} disabled />
)}
<Popover position={Position.BOTTOM_LEFT} content={this.renderExtraMenu()}>
<Button icon={IconNames.MORE} />
<Button icon={IconNames.MORE} intent={Intent.PRIMARY} />
</Popover>
</ButtonGroup>
);