diff --git a/web-console/script/create-sql-function-doc.js b/web-console/script/create-sql-function-doc.js index dc55989a2ae..d4553a692f0 100755 --- a/web-console/script/create-sql-function-doc.js +++ b/web-console/script/create-sql-function-doc.js @@ -23,7 +23,41 @@ const fs = require('fs-extra'); const readfile = '../docs/content/querying/sql.md'; const writefile = 'lib/sql-function-doc.ts'; -const heading = `/* +const readDoc = async () => { + const data = await fs.readFile(readfile, 'utf-8'); + const lines = data.split('\n'); + + const functionDocs = []; + const dataTypeDocs = []; + for (let line of lines) { + const functionMatch = line.match(/^\|`(.+\(.*\))`\|(.+)\|$/); + if (functionMatch) { + functionDocs.push({ + syntax: functionMatch[1], + description: functionMatch[2] + }) + } + + const dataTypeMatch = line.match(/^\|([A-Z]+)\|([A-Z]+)\|(.*)\|(.*)\|$/); + if (dataTypeMatch) { + dataTypeDocs.push({ + syntax: dataTypeMatch[1], + description: dataTypeMatch[4] || `Druid runtime type: ${dataTypeMatch[2]}` + }) + } + } + + // Make sure there are at least 10 functions for sanity + if (functionDocs.length < 10) { + throw new Error(`Did not find enough function entries did the structure of '${readfile}' change? (found ${functionDocs.length})`); + } + + // Make sure there are at least 5 data types for sanity + if (dataTypeDocs.length < 10) { + throw new Error(`Did not find enough data type entries did the structure of '${readfile}' change? (found ${dataTypeDocs.length})`); + } + + const content = `/* * 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 @@ -43,65 +77,19 @@ const heading = `/* // This file is auto generated and should not be modified -export interface FunctionDescription { +export interface SyntaxDescription { syntax: string; description: string; } /* tslint:disable:quotemark */ -export const SQLFunctionDoc: FunctionDescription[] = `; +export const SQL_FUNCTIONS: SyntaxDescription[] = ${JSON.stringify(functionDocs, null, 2)}; -const readDoc = async () => { - try { - const data = await fs.readFile(readfile, 'utf-8'); - const sections = data.split("##"); +export const SQL_DATE_TYPES: SyntaxDescription[] = ${JSON.stringify(dataTypeDocs, null, 2)}; +`; - let entries = []; - sections.forEach((section) => { - if (!/^#.*function/.test(section)) return; - - entries = entries.concat( - section.split('\n').map(line => { - if (line.startsWith('|`')) { - const rawSyntax = line.match(/\|`(.*)`\|/); - if (rawSyntax == null) return null; - const syntax = rawSyntax[1] - .replace(/\\/g,'') - .replace(/|/g,'|'); - - // Must have an uppercase letter - if (!/[A-Z]/.test(syntax)) return null; - - const rawDescription = line.match(/`\|(.*)\|/); - if (rawDescription == null) return null; - const description = rawDescription[1]; - - return { - syntax: syntax, - description: description - }; - } - }).filter(Boolean) - ); - }); - - // Make sure there are at least 10 functions for sanity - if (entries.length < 10) { - throw new Error(`Did not find any entries did the structure of '${readfile}' change?`); - } - - const content = heading + JSON.stringify(entries, null, 2) + ';\n'; - - try { - await fs.writeFile(writefile, content, 'utf-8'); - } catch (e) { - console.log(`Error when writing to ${writefile}: `, e); - } - - } catch (e) { - console.log(`Error when reading ${readfile}: `, e); - } -} + await fs.writeFile(writefile, content, 'utf-8'); +}; readDoc(); diff --git a/web-console/src/views/query-view/column-tree/column-tree.tsx b/web-console/src/views/query-view/column-tree/column-tree.tsx index b3b2213fda4..5e92b0f089f 100644 --- a/web-console/src/views/query-view/column-tree/column-tree.tsx +++ b/web-console/src/views/query-view/column-tree/column-tree.tsx @@ -150,10 +150,12 @@ export class ColumnTree extends React.PureComponent= CURRENT_TIMESTAMP - INTERVAL '1' DAY`); } else { - onQueryStringChange(`SELECT * FROM ${tableSchema}.${nodeData.label}`); + onQueryStringChange(`SELECT * +FROM ${tableSchema}.${nodeData.label}`); } break; diff --git a/web-console/src/views/query-view/query-input/keywords.ts b/web-console/src/views/query-view/query-input/keywords.ts new file mode 100644 index 00000000000..34d80fb975e --- /dev/null +++ b/web-console/src/views/query-view/query-input/keywords.ts @@ -0,0 +1,81 @@ +/* + * 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. + */ + +// Hand picked from https://druid.apache.org/docs/latest/querying/sql.html + +export const SQL_KEYWORDS: string[] = [ + 'EXPLAIN PLAN FOR', + 'WITH', + 'AS', + 'SELECT', + 'ALL', + 'DISTINCT', + 'FROM', + 'WHERE', + 'GROUP BY', + 'HAVING', + 'ORDER BY', + 'ASC', + 'DESC', + 'LIMIT', + 'UNION ALL' +]; + +export const SQL_EXPRESSION_PARTS: string[] = [ + 'FILTER', + 'END', + 'ELSE', + 'WHEN', + 'CASE', + 'OR', + 'AND', + 'NOT', + 'IN', + 'IS', + 'TO', + 'BETWEEN', + 'LIKE', + 'ESCAPE', + 'BOTH', + 'LEADING', + 'TRAILING', + 'EPOCH', + 'SECOND', + 'MINUTE', + 'HOUR', + 'DAY', + 'DOW', + 'DOY', + 'WEEK', + 'MONTH', + 'QUARTER', + 'YEAR', + 'TIMESTAMP', + 'INTERVAL' +]; + +export const SQL_CONSTANTS: string[] = [ + 'NULL', + 'FALSE', + 'TRUE' +]; + +export const SQL_DYNAMICS: string[] = [ + 'CURRENT_TIMESTAMP', + 'CURRENT_DATE' +]; diff --git a/web-console/src/views/query-view/query-input/query-input.tsx b/web-console/src/views/query-view/query-input/query-input.tsx index ac7ad658ebd..e52684f675d 100644 --- a/web-console/src/views/query-view/query-input/query-input.tsx +++ b/web-console/src/views/query-view/query-input/query-input.tsx @@ -16,17 +16,19 @@ * limitations under the License. */ -import { IResizeEntry, ITreeNode, ResizeSensor } from '@blueprintjs/core'; +import { IResizeEntry, ResizeSensor } from '@blueprintjs/core'; import ace from 'brace'; import React from 'react'; import AceEditor from 'react-ace'; import ReactDOMServer from 'react-dom/server'; -import { SQLFunctionDoc } from '../../../../lib/sql-function-doc'; +import { SQL_DATE_TYPES, SQL_FUNCTIONS, SyntaxDescription } from '../../../../lib/sql-function-doc'; import { uniq } from '../../../utils'; import { ColumnMetadata } from '../../../utils/column-metadata'; import { ColumnTreeProps, ColumnTreeState } from '../column-tree/column-tree'; +import { SQL_CONSTANTS, SQL_DYNAMICS, SQL_EXPRESSION_PARTS, SQL_KEYWORDS } from './keywords'; + import './query-input.scss'; const langTools = ace.acequire('ace/ext/language_tools'); @@ -46,7 +48,6 @@ export interface QueryInputState { } export class QueryInput extends React.PureComponent { - static getDerivedStateFromProps(props: ColumnTreeProps, state: ColumnTreeState) { const { columnMetadata } = props; @@ -80,33 +81,29 @@ export class QueryInput extends React.PureComponent { if (!langTools) return; - /* - Please refer to the source code @ - https://github.com/ajaxorg/ace/blob/9b5b63d1dc7c1b81b58d30c87d14b5905d030ca5/lib/ace/ext/language_tools.js#L41 - for the implementation of keyword completer - */ + + const keywordList = ([] as any[]).concat( + SQL_KEYWORDS.map(v => ({ name: v, value: v, score: 0, meta: 'keyword' })), + 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' })) + ); + const keywordCompleter = { getCompletions: (editor: any, session: any, pos: any, prefix: any, callback: any) => { - if (session.$mode.completer) { - return session.$mode.completer.getCompletions(editor, session, pos, prefix, callback); - } - const state = editor.session.getState(pos.row); - let keywordCompletions = session.$mode.getCompletions(state, session, pos, prefix); - keywordCompletions = keywordCompletions.map((d: any) => { - return Object.assign(d, {name: d.name.toUpperCase(), value: d.value.toUpperCase()}); - }); - return callback(null, keywordCompletions); + return callback(null, keywordList); } }; + langTools.setCompleters([langTools.snippetCompleter, langTools.textCompleter, keywordCompleter]); } private addFunctionAutoCompleter = (): void => { if (!langTools) return; - const functionList: any[] = SQLFunctionDoc.map((entry: any) => { - let funcName: string = entry.syntax.replace(/\(.*\)/, '()'); - if (!funcName.includes('(')) funcName = funcName.substr(0, 10); + const functionList: any[] = SQL_FUNCTIONS.map((entry: SyntaxDescription) => { + const funcName: string = entry.syntax.replace(/\(.*\)/, '()'); return { value: funcName, score: 80, diff --git a/web-console/src/views/query-view/run-button/run-button.tsx b/web-console/src/views/query-view/run-button/run-button.tsx index 8b8e3de5de5..b79c256cb61 100644 --- a/web-console/src/views/query-view/run-button/run-button.tsx +++ b/web-console/src/views/query-view/run-button/run-button.tsx @@ -90,7 +90,7 @@ export class RunButton extends React.PureComponent