This is an automated email from the ASF dual-hosted git repository.
vogievetsky pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new 6d8799f Update QueryView to use latest DruidQueryToolkit (#10201)
6d8799f is described below
commit 6d8799f2dfec72c234f408a3433cf93f69c99ddb
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Thu Jul 23 22:45:01 2020 -0700
Update QueryView to use latest DruidQueryToolkit (#10201)
* Update to latest DruidQueryToolkit
* add THEN keyword
* do not crash on invalid JSON
---
licenses.yaml | 2 +-
web-console/lib/keywords.js | 6 +
web-console/package-lock.json | 6 +-
web-console/package.json | 2 +-
web-console/script/create-sql-docs.js | 10 +-
web-console/src/utils/general.tsx | 4 -
web-console/src/utils/index.tsx | 1 +
web-console/src/utils/query-cursor.ts | 64 +++
.../src/views/datasource-view/datasource-view.tsx | 4 +-
.../__snapshots__/query-view.spec.tsx.snap | 4 +-
.../number-menu-items/number-menu-items.spec.tsx | 6 +-
.../number-menu-items/number-menu-items.tsx | 243 +++--------
.../string-menu-items/string-menu-items.spec.tsx | 6 +-
.../string-menu-items/string-menu-items.tsx | 245 +++++------
.../time-menu-items/time-menu-items.spec.tsx | 6 +-
.../time-menu-items/time-menu-items.tsx | 470 ++++++---------------
.../query-view/column-tree/column-tree.spec.tsx | 6 +-
.../views/query-view/column-tree/column-tree.tsx | 346 ++++++++-------
.../__snapshots__/query-extra-info.spec.tsx.snap | 2 +-
.../query-extra-info/query-extra-info.spec.tsx | 11 +-
.../query-extra-info/query-extra-info.tsx | 50 +--
.../views/query-view/query-input/query-input.tsx | 16 +-
.../__snapshots__/query-output.spec.tsx.snap | 38 ++
.../query-view/query-output/query-output.scss | 6 +
.../query-view/query-output/query-output.spec.tsx | 15 +-
.../views/query-view/query-output/query-output.tsx | 330 ++++++++-------
web-console/src/views/query-view/query-view.tsx | 209 ++++-----
27 files changed, 970 insertions(+), 1138 deletions(-)
diff --git a/licenses.yaml b/licenses.yaml
index e73c484..a83f20d 100644
--- a/licenses.yaml
+++ b/licenses.yaml
@@ -4707,7 +4707,7 @@ license_category: binary
module: web-console
license_name: Apache License version 2.0
copyright: Imply Data
-version: 0.6.1
+version: 0.8.4
---
diff --git a/web-console/lib/keywords.js b/web-console/lib/keywords.js
index 09f4b27..69a3f87 100644
--- a/web-console/lib/keywords.js
+++ b/web-console/lib/keywords.js
@@ -28,6 +28,9 @@ exports.SQL_KEYWORDS = [
'FROM',
'WHERE',
'GROUP BY',
+ 'CUBE',
+ 'ROLLUP',
+ 'GROUPING SETS',
'HAVING',
'ORDER BY',
'ASC',
@@ -48,6 +51,7 @@ exports.SQL_EXPRESSION_PARTS = [
'END',
'ELSE',
'WHEN',
+ 'THEN',
'CASE',
'OR',
'AND',
@@ -56,7 +60,9 @@ exports.SQL_EXPRESSION_PARTS = [
'IS',
'TO',
'BETWEEN',
+ 'SYMMETRIC',
'LIKE',
+ 'SIMILAR',
'ESCAPE',
'BOTH',
'LEADING',
diff --git a/web-console/package-lock.json b/web-console/package-lock.json
index 52de297..2f8012b 100644
--- a/web-console/package-lock.json
+++ b/web-console/package-lock.json
@@ -4243,9 +4243,9 @@
}
},
"druid-query-toolkit": {
- "version": "0.6.1",
- "resolved":
"https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.6.1.tgz",
- "integrity":
"sha512-ykrWD9AbDQEvE55x8ST1kyiiGHSN8zhp/Lqe7z43/l7XG9QD7AQwfBzOn+HATXFynrOQN/3z3Cis70EzdDjc1g==",
+ "version": "0.8.4",
+ "resolved":
"https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.8.4.tgz",
+ "integrity":
"sha512-d0/OJDh6xNxlmqu874v1K2yGH0DD5ZXhRGR8iRFNDRUUZzTLSIdOTyaulHxCQ8j1bpfhB6VN+XTWzd0V6AgqbQ==",
"requires": {
"tslib": "^1.10.0"
}
diff --git a/web-console/package.json b/web-console/package.json
index 3e517d4..da46a80 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -68,7 +68,7 @@
"d3-axis": "^1.0.12",
"d3-scale": "^3.2.0",
"d3-selection": "^1.4.0",
- "druid-query-toolkit": "^0.6.1",
+ "druid-query-toolkit": "^0.8.4",
"file-saver": "^2.0.2",
"has-own-prop": "^2.0.0",
"hjson": "^3.2.1",
diff --git a/web-console/script/create-sql-docs.js
b/web-console/script/create-sql-docs.js
index 092706d..ce09be3 100755
--- a/web-console/script/create-sql-docs.js
+++ b/web-console/script/create-sql-docs.js
@@ -23,6 +23,10 @@ const fs = require('fs-extra');
const readfile = '../docs/querying/sql.md';
const writefile = 'lib/sql-docs.js';
+function unwrapMarkdownLinks(str) {
+ return str.replace(/\[([^\]]+)\]\([^)]+\)/g, (_, s) => s);
+}
+
const readDoc = async () => {
const data = await fs.readFile(readfile, 'utf-8');
const lines = data.split('\n');
@@ -35,7 +39,7 @@ const readDoc = async () => {
functionDocs.push({
name: functionMatch[1],
arguments: functionMatch[2],
- description: functionMatch[3],
+ description: unwrapMarkdownLinks(functionMatch[3]),
});
}
@@ -43,7 +47,9 @@ const readDoc = async () => {
if (dataTypeMatch) {
dataTypeDocs.push({
name: dataTypeMatch[1],
- description: dataTypeMatch[4] || `Druid runtime type:
${dataTypeMatch[2]}`,
+ description: unwrapMarkdownLinks(
+ dataTypeMatch[4] || `Druid runtime type: ${dataTypeMatch[2]}`,
+ ),
});
}
}
diff --git a/web-console/src/utils/general.tsx
b/web-console/src/utils/general.tsx
index 2dc43ae..1ff1eaf 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -309,10 +309,6 @@ export function downloadFile(text: string, type: string,
filename: string): void
FileSaver.saveAs(blob, filename);
}
-export function escapeSqlIdentifier(identifier: string): string {
- return `"${identifier.replace(/"/g, '""')}"`;
-}
-
export function copyAndAlert(copyString: string, alertMessage: string): void {
copy(copyString, { format: 'text/plain' });
AppToaster.show({
diff --git a/web-console/src/utils/index.tsx b/web-console/src/utils/index.tsx
index 89b4347..7e1cca2 100644
--- a/web-console/src/utils/index.tsx
+++ b/web-console/src/utils/index.tsx
@@ -20,4 +20,5 @@ export * from './general';
export * from './druid-query';
export * from './query-manager';
export * from './query-state';
+export * from './query-cursor';
export * from './local-storage-keys';
diff --git a/web-console/src/utils/query-cursor.ts
b/web-console/src/utils/query-cursor.ts
new file mode 100644
index 0000000..4eb32fe
--- /dev/null
+++ b/web-console/src/utils/query-cursor.ts
@@ -0,0 +1,64 @@
+/*
+ * 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 { SqlBase, SqlLiteral, SqlQuery } from 'druid-query-toolkit';
+
+export const EMPTY_LITERAL = SqlLiteral.create('');
+
+const CRAZY_STRING = '[email protected].$';
+const DOT_DOT_DOT_LITERAL = SqlLiteral.create('...');
+
+export function prettyPrintSql(b: SqlBase): string {
+ return b
+ .walk(b => {
+ if (b === EMPTY_LITERAL) {
+ return DOT_DOT_DOT_LITERAL;
+ }
+ return b;
+ })
+ .prettyTrim(50)
+ .toString();
+}
+
+export interface RowColumn {
+ row: number;
+ column: number;
+}
+
+export function findEmptyLiteralPosition(query: SqlQuery): RowColumn |
undefined {
+ const subQueryString = query
+ .walk(b => {
+ if (b === EMPTY_LITERAL) {
+ return SqlLiteral.create(CRAZY_STRING);
+ }
+ return b;
+ })
+ .toString();
+
+ const crazyIndex = subQueryString.indexOf(CRAZY_STRING);
+ if (crazyIndex < 0) return;
+
+ const prefix = subQueryString.substr(0, crazyIndex);
+ const lines = prefix.split(/\n/g);
+ const row = lines.length - 1;
+ const lastLine = lines[row];
+ return {
+ row,
+ column: lastLine.length,
+ };
+}
diff --git a/web-console/src/views/datasource-view/datasource-view.tsx
b/web-console/src/views/datasource-view/datasource-view.tsx
index c715fd5..2b9cf31 100644
--- a/web-console/src/views/datasource-view/datasource-view.tsx
+++ b/web-console/src/views/datasource-view/datasource-view.tsx
@@ -20,6 +20,7 @@ import { FormGroup, InputGroup, Intent, MenuItem, Switch }
from '@blueprintjs/co
import { IconNames } from '@blueprintjs/icons';
import axios from 'axios';
import classNames from 'classnames';
+import { SqlQuery, SqlRef } from 'druid-query-toolkit';
import React from 'react';
import ReactTable, { Filter } from 'react-table';
@@ -41,7 +42,6 @@ import { AppToaster } from '../../singletons/toaster';
import {
addFilter,
countBy,
- escapeSqlIdentifier,
formatBytes,
formatNumber,
getDruidErrorMessage,
@@ -598,7 +598,7 @@ GROUP BY 1`;
{
icon: IconNames.APPLICATION,
title: 'Query with SQL',
- onAction: () => goToQuery(`SELECT * FROM
${escapeSqlIdentifier(datasource)}`),
+ onAction: () =>
goToQuery(SqlQuery.create(SqlRef.table(datasource)).toString()),
},
{
icon: IconNames.GANTT_CHART,
diff --git
a/web-console/src/views/query-view/__snapshots__/query-view.spec.tsx.snap
b/web-console/src/views/query-view/__snapshots__/query-view.spec.tsx.snap
index 4348898..1f9b7f5 100644
--- a/web-console/src/views/query-view/__snapshots__/query-view.spec.tsx.snap
+++ b/web-console/src/views/query-view/__snapshots__/query-view.spec.tsx.snap
@@ -8,7 +8,7 @@ exports[`sql view matches snapshot 1`] = `
columnMetadataLoading={true}
defaultSchema="druid"
getParsedQuery={[Function]}
- onQueryStringChange={[Function]}
+ onQueryChange={[Function]}
/>
<t
customClassName=""
@@ -90,7 +90,7 @@ exports[`sql view matches snapshot with query 1`] = `
columnMetadataLoading={true}
defaultSchema="druid"
getParsedQuery={[Function]}
- onQueryStringChange={[Function]}
+ onQueryChange={[Function]}
/>
<t
customClassName=""
diff --git
a/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.spec.tsx
b/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.spec.tsx
index 53b2bd5..ed77abe 100644
---
a/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.spec.tsx
+++
b/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.spec.tsx
@@ -17,7 +17,7 @@
*/
import { render } from '@testing-library/react';
-import { parseSqlQuery } from 'druid-query-toolkit';
+import { SqlQuery } from 'druid-query-toolkit';
import React from 'react';
import { NumberMenuItems } from './number-menu-items';
@@ -29,7 +29,7 @@ describe('number menu', () => {
schema="schema"
table="table"
columnName={'added'}
- parsedQuery={parseSqlQuery(`SELECT channel, count(*) as cnt FROM
wikipedia GROUP BY 1`)}
+ parsedQuery={SqlQuery.parse(`SELECT channel, count(*) as cnt FROM
wikipedia GROUP BY 1`)}
onQueryChange={() => {}}
/>
);
@@ -44,7 +44,7 @@ describe('number menu', () => {
schema="schema"
table="table"
columnName={'added'}
- parsedQuery={parseSqlQuery(`SELECT added, count(*) as cnt FROM
wikipedia GROUP BY 1`)}
+ parsedQuery={SqlQuery.parse(`SELECT added, count(*) as cnt FROM
wikipedia GROUP BY 1`)}
onQueryChange={() => {}}
/>
);
diff --git
a/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.tsx
b/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.tsx
index ca8dc28..0c5d2b8 100644
---
a/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.tsx
+++
b/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.tsx
@@ -18,240 +18,134 @@
import { MenuItem } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
-import {
- SqlAliasRef,
- SqlFunction,
- SqlLiteral,
- SqlMulti,
- SqlQuery,
- SqlRef,
-} from 'druid-query-toolkit';
+import { SqlExpression, SqlFunction, SqlLiteral, SqlQuery, SqlRef } from
'druid-query-toolkit';
import React from 'react';
-import { getCurrentColumns } from '../../column-tree';
+import { prettyPrintSql } from '../../../../../utils';
+
+const NINE_THOUSAND = SqlLiteral.create(9000);
export interface NumberMenuItemsProps {
table: string;
schema: string;
columnName: string;
parsedQuery: SqlQuery;
- onQueryChange: (queryString: SqlQuery, run?: boolean) => void;
+ onQueryChange: (query: SqlQuery, run?: boolean) => void;
}
export const NumberMenuItems = React.memo(function NumberMenuItems(props:
NumberMenuItemsProps) {
function renderFilterMenu(): JSX.Element {
const { columnName, parsedQuery, onQueryChange } = props;
+ const ref = SqlRef.column(columnName);
- return (
- <MenuItem icon={IconNames.FILTER} text={`Filter`}>
- <MenuItem
- text={`"${columnName}" > 100`}
- onClick={() => {
- onQueryChange(
- parsedQuery.addWhereFilter(
- SqlRef.fromStringWithDoubleQuotes(columnName),
- '>',
- SqlLiteral.fromInput(100),
- ),
- );
- }}
- />
+ function filterMenuItem(clause: SqlExpression) {
+ return (
<MenuItem
- text={`"${columnName}" <= 100`}
+ text={prettyPrintSql(clause)}
onClick={() => {
- onQueryChange(
- parsedQuery.addWhereFilter(
- SqlRef.fromStringWithDoubleQuotes(columnName),
- '<=',
- SqlLiteral.fromInput(100),
- ),
- );
+ onQueryChange(parsedQuery.addToWhere(clause));
}}
/>
+ );
+ }
+
+ return (
+ <MenuItem icon={IconNames.FILTER} text={`Filter`}>
+ {filterMenuItem(ref.greaterThan(NINE_THOUSAND))}
+ {filterMenuItem(ref.lessThanOrEqual(NINE_THOUSAND))}
</MenuItem>
);
}
function renderRemoveFilter(): JSX.Element | undefined {
const { columnName, parsedQuery, onQueryChange } = props;
- if (!parsedQuery.getCurrentFilters().includes(columnName)) return;
+ if (!parsedQuery.getEffectiveWhereExpression().containsColumn(columnName))
return;
return (
<MenuItem
icon={IconNames.FILTER_REMOVE}
text={`Remove filter`}
onClick={() => {
- onQueryChange(parsedQuery.removeFilter(columnName), true);
+ onQueryChange(parsedQuery.removeColumnFromWhere(columnName), true);
}}
/>
);
}
+ function renderGroupByMenu(): JSX.Element | undefined {
+ const { columnName, parsedQuery, onQueryChange } = props;
+ if (!parsedQuery.hasGroupBy()) return;
+ const ref = SqlRef.column(columnName);
+
+ function groupByMenuItem(ex: SqlExpression, alias?: string) {
+ return (
+ <MenuItem
+ text={prettyPrintSql(ex)}
+ onClick={() => {
+ onQueryChange(parsedQuery.addToGroupBy(ex.as(alias)), true);
+ }}
+ />
+ );
+ }
+
+ return (
+ <MenuItem icon={IconNames.GROUP_OBJECTS} text={`Group by`}>
+ {groupByMenuItem(ref)}
+ {groupByMenuItem(
+ SqlFunction.simple('TRUNC', [ref, SqlLiteral.create(-1)]),
+ `${columnName}_truncated`,
+ )}
+ </MenuItem>
+ );
+ }
+
function renderRemoveGroupBy(): JSX.Element | undefined {
const { columnName, parsedQuery, onQueryChange } = props;
- if (!parsedQuery.hasGroupByColumn(columnName)) return;
+ const selectIndex = parsedQuery.getSelectIndexForColumn(columnName);
+ if (!parsedQuery.isGroupedSelectIndex(selectIndex)) return;
+
return (
<MenuItem
icon={IconNames.UNGROUP_OBJECTS}
text={'Remove group by'}
onClick={() => {
- onQueryChange(parsedQuery.removeFromGroupBy(columnName), true);
+ onQueryChange(parsedQuery.removeSelectIndex(selectIndex), true);
}}
/>
);
}
- function renderGroupByMenu(): JSX.Element | undefined {
+ function renderAggregateMenu(): JSX.Element | undefined {
const { columnName, parsedQuery, onQueryChange } = props;
- if (!parsedQuery.groupByExpression) return;
+ if (!parsedQuery.hasGroupBy()) return;
+ const ref = SqlRef.column(columnName);
- return (
- <MenuItem icon={IconNames.GROUP_OBJECTS} text={`Group by`}>
+ function aggregateMenuItem(ex: SqlExpression, alias: string) {
+ return (
<MenuItem
- text={`"${columnName}"`}
+ text={prettyPrintSql(ex)}
onClick={() => {
- onQueryChange(
-
parsedQuery.addToGroupBy(SqlRef.fromStringWithDoubleQuotes(columnName)),
- true,
- );
+ onQueryChange(parsedQuery.addSelectExpression(ex.as(alias)), true);
}}
/>
- <MenuItem
- text={`TRUNC("${columnName}", -1) AS "${columnName}_trunc"`}
- onClick={() => {
- onQueryChange(
- parsedQuery.addToGroupBy(
- SqlAliasRef.sqlAliasFactory(
- SqlFunction.sqlFunctionFactory('TRUNC', [
- SqlRef.fromStringWithDoubleQuotes(columnName),
- SqlLiteral.fromInput(-1),
- ]),
- `${columnName}_truncated`,
- ),
- ),
- true,
- );
- }}
- />
- </MenuItem>
- );
- }
-
- function renderAggregateMenu(): JSX.Element | undefined {
- const { columnName, parsedQuery, onQueryChange } = props;
- if (!parsedQuery.groupByExpression) return;
+ );
+ }
return (
<MenuItem icon={IconNames.FUNCTION} text={`Aggregate`}>
- <MenuItem
- text={`SUM(${columnName}) AS "sum_${columnName}"`}
- onClick={() => {
- onQueryChange(
- parsedQuery.addAggregateColumn(
- [SqlRef.fromString(columnName)],
- 'SUM',
- `sum_${columnName}`,
- ),
- true,
- );
- }}
- />
- <MenuItem
- text={`MAX(${columnName}) AS "max_${columnName}"`}
- onClick={() => {
- onQueryChange(
- parsedQuery.addAggregateColumn(
- [SqlRef.fromString(columnName)],
- 'MAX',
- `max_${columnName}`,
- ),
- true,
- );
- }}
- />
- <MenuItem
- text={`MIN(${columnName}) AS "min_${columnName}"`}
- onClick={() => {
- onQueryChange(
- parsedQuery.addAggregateColumn(
- [SqlRef.fromString(columnName)],
- 'MIN',
- `min_${columnName}`,
- ),
- true,
- );
- }}
- />
+ {aggregateMenuItem(SqlFunction.simple('SUM', [ref]),
`sum_${columnName}`)}
+ {aggregateMenuItem(SqlFunction.simple('MIN', [ref]),
`min_${columnName}`)}
+ {aggregateMenuItem(SqlFunction.simple('MAX', [ref]),
`max_${columnName}`)}
+ {aggregateMenuItem(SqlFunction.simple('AVG', [ref]),
`avg_${columnName}`)}
+ {aggregateMenuItem(
+ SqlFunction.simple('APPROX_QUANTILE', [ref,
SqlLiteral.create(0.98)]),
+ `p98_${columnName}`,
+ )}
+ {aggregateMenuItem(SqlFunction.simple('LATEST', [ref]),
`latest_${columnName}`)}
</MenuItem>
);
}
- function renderJoinMenu(): JSX.Element | undefined {
- const { schema, table, columnName, parsedQuery, onQueryChange } = props;
- if (schema !== 'lookup' || !parsedQuery) return;
-
- const { originalTableColumn, lookupColumn } =
getCurrentColumns(parsedQuery, table);
-
- return (
- <>
- <MenuItem
- icon={IconNames.JOIN_TABLE}
- text={parsedQuery.joinTable ? `Replace join` : `Join`}
- >
- <MenuItem
- icon={IconNames.LEFT_JOIN}
- text={`Left join`}
- onClick={() => {
- onQueryChange(
- parsedQuery.addJoin(
- 'LEFT',
- SqlRef.fromString(table, schema).upgrade(),
- SqlMulti.sqlMultiFactory('=', [
- SqlRef.fromString(columnName, table, 'lookup'),
- SqlRef.fromString(
- lookupColumn === columnName ? originalTableColumn :
'XXX',
- parsedQuery.getTableName(),
- ),
- ]),
- ),
- false,
- );
- }}
- />
- <MenuItem
- icon={IconNames.INNER_JOIN}
- text={`Inner join`}
- onClick={() => {
- onQueryChange(
- parsedQuery.addJoin(
- 'INNER',
- SqlRef.fromString(table, schema).upgrade(),
- SqlMulti.sqlMultiFactory('=', [
- SqlRef.fromString(columnName, table, 'lookup'),
- SqlRef.fromString(
- lookupColumn === columnName ? originalTableColumn :
'XXX',
- parsedQuery.getTableName(),
- ),
- ]),
- ),
- false,
- );
- }}
- />
- </MenuItem>
- {parsedQuery.onExpression &&
- parsedQuery.onExpression instanceof SqlMulti &&
- parsedQuery.onExpression.containsColumn(columnName) && (
- <MenuItem
- icon={IconNames.EXCHANGE}
- text={`Remove join`}
- onClick={() => onQueryChange(parsedQuery.removeJoin())}
- />
- )}
- </>
- );
- }
-
return (
<>
{renderFilterMenu()}
@@ -259,7 +153,6 @@ export const NumberMenuItems = React.memo(function
NumberMenuItems(props: Number
{renderGroupByMenu()}
{renderRemoveGroupBy()}
{renderAggregateMenu()}
- {renderJoinMenu()}
</>
);
});
diff --git
a/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.spec.tsx
b/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.spec.tsx
index bbaae61..3384a94 100644
---
a/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.spec.tsx
+++
b/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.spec.tsx
@@ -17,7 +17,7 @@
*/
import { render } from '@testing-library/react';
-import { parseSqlQuery } from 'druid-query-toolkit';
+import { SqlQuery } from 'druid-query-toolkit';
import React from 'react';
import { StringMenuItems } from './string-menu-items';
@@ -29,7 +29,7 @@ describe('string menu', () => {
table={'table'}
schema={'schema'}
columnName={'cityName'}
- parsedQuery={parseSqlQuery(`SELECT channel, count(*) as cnt FROM
wikipedia GROUP BY 1`)}
+ parsedQuery={SqlQuery.parse(`SELECT channel, count(*) as cnt FROM
wikipedia GROUP BY 1`)}
onQueryChange={() => {}}
/>
);
@@ -44,7 +44,7 @@ describe('string menu', () => {
table={'table'}
schema={'schema'}
columnName={'channel'}
- parsedQuery={parseSqlQuery(`SELECT channel, count(*) as cnt FROM
wikipedia GROUP BY 1`)}
+ parsedQuery={SqlQuery.parse(`SELECT channel, count(*) as cnt FROM
wikipedia GROUP BY 1`)}
onQueryChange={() => {}}
/>
);
diff --git
a/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx
b/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx
index dcba6f0..4e69cb4 100644
---
a/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx
+++
b/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx
@@ -19,57 +19,62 @@
import { MenuItem } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import {
- SqlAliasRef,
+ SqlExpression,
SqlFunction,
+ SqlJoinPart,
SqlLiteral,
- SqlMulti,
SqlQuery,
SqlRef,
} from 'druid-query-toolkit';
import React from 'react';
-import { getCurrentColumns } from '../../column-tree';
+import { EMPTY_LITERAL, prettyPrintSql } from '../../../../../utils';
+import { getJoinColumns } from '../../column-tree';
export interface StringMenuItemsProps {
schema: string;
table: string;
columnName: string;
parsedQuery: SqlQuery;
- onQueryChange: (queryString: SqlQuery, run?: boolean) => void;
+ onQueryChange: (query: SqlQuery, run?: boolean) => void;
}
export const StringMenuItems = React.memo(function StringMenuItems(props:
StringMenuItemsProps) {
function renderFilterMenu(): JSX.Element | undefined {
const { columnName, parsedQuery, onQueryChange } = props;
+ const ref = SqlRef.column(columnName);
- return (
- <MenuItem icon={IconNames.FILTER} text={`Filter`}>
- <MenuItem
- text={`"${columnName}" = 'xxx'`}
- onClick={() => {
- onQueryChange(parsedQuery.addWhereFilter(columnName, '=', 'xxx'),
false);
- }}
- />
+ function filterMenuItem(clause: SqlExpression, run = true) {
+ return (
<MenuItem
- text={`"${columnName}" LIKE '%xxx%'`}
+ text={prettyPrintSql(clause)}
onClick={() => {
- onQueryChange(parsedQuery.addWhereFilter(columnName, 'LIKE',
'%xxx%'), false);
+ onQueryChange(parsedQuery.addToWhere(clause), run);
}}
/>
+ );
+ }
+
+ return (
+ <MenuItem icon={IconNames.FILTER} text={`Filter`}>
+ {filterMenuItem(ref.isNotNull())}
+ {filterMenuItem(ref.equal(EMPTY_LITERAL), false)}
+ {filterMenuItem(ref.like(EMPTY_LITERAL), false)}
+ {filterMenuItem(SqlFunction.simple('REGEXP_LIKE', [ref,
EMPTY_LITERAL]), false)}
</MenuItem>
);
}
function renderRemoveFilter(): JSX.Element | undefined {
const { columnName, parsedQuery, onQueryChange } = props;
- if (!parsedQuery.getCurrentFilters().includes(columnName)) return;
+ if (!parsedQuery.getEffectiveWhereExpression().containsColumn(columnName))
return;
return (
<MenuItem
icon={IconNames.FILTER_REMOVE}
text={`Remove filter`}
onClick={() => {
- onQueryChange(parsedQuery.removeFilter(columnName), true);
+ onQueryChange(parsedQuery.removeColumnFromWhere(columnName), true);
}}
/>
);
@@ -77,13 +82,15 @@ export const StringMenuItems = React.memo(function
StringMenuItems(props: String
function renderRemoveGroupBy(): JSX.Element | undefined {
const { columnName, parsedQuery, onQueryChange } = props;
- if (!parsedQuery.hasGroupByColumn(columnName)) return;
+ const selectIndex = parsedQuery.getSelectIndexForColumn(columnName);
+ if (!parsedQuery.isGroupedSelectIndex(selectIndex)) return;
+
return (
<MenuItem
icon={IconNames.UNGROUP_OBJECTS}
text={'Remove group by'}
onClick={() => {
- onQueryChange(parsedQuery.removeFromGroupBy(columnName), true);
+ onQueryChange(parsedQuery.removeSelectIndex(selectIndex), true);
}}
/>
);
@@ -91,78 +98,69 @@ export const StringMenuItems = React.memo(function
StringMenuItems(props: String
function renderGroupByMenu(): JSX.Element | undefined {
const { columnName, parsedQuery, onQueryChange } = props;
- if (!parsedQuery.groupByExpression) return;
+ if (!parsedQuery.hasGroupBy()) return;
- return (
- <MenuItem icon={IconNames.GROUP_OBJECTS} text={`Group by`}>
- <MenuItem
- text={`"${columnName}"`}
- onClick={() => {
- onQueryChange(
-
parsedQuery.addToGroupBy(SqlRef.fromStringWithDoubleQuotes(columnName)),
- true,
- );
- }}
- />
+ function groupByMenuItem(ex: SqlExpression, alias?: string) {
+ return (
<MenuItem
- text={`SUBSTRING("${columnName}", 1, 2) AS
"${columnName}_substring"`}
+ text={prettyPrintSql(ex)}
onClick={() => {
- onQueryChange(
- parsedQuery.addToGroupBy(
- SqlAliasRef.sqlAliasFactory(
- SqlFunction.sqlFunctionFactory('SUBSTRING', [
- SqlRef.fromStringWithDoubleQuotes(columnName),
- SqlLiteral.fromInput(1),
- SqlLiteral.fromInput(2),
- ]),
- `${columnName}_substring`,
- ),
- ),
- true,
- );
+ onQueryChange(parsedQuery.addToGroupBy(alias ? ex.as(alias) : ex),
true);
}}
/>
+ );
+ }
+
+ return (
+ <MenuItem icon={IconNames.GROUP_OBJECTS} text={`Group by`}>
+ {groupByMenuItem(SqlRef.column(columnName))}
+ {groupByMenuItem(
+ SqlFunction.simple('SUBSTRING', [
+ SqlRef.column(columnName),
+ SqlLiteral.create(1),
+ SqlLiteral.create(2),
+ ]),
+ `${columnName}_substring`,
+ )}
+ {groupByMenuItem(
+ SqlFunction.simple('REGEXP_EXTRACT', [
+ SqlRef.column(columnName),
+ SqlLiteral.create('(\\d+)'),
+ ]),
+ `${columnName}_extract`,
+ )}
</MenuItem>
);
}
function renderAggregateMenu(): JSX.Element | undefined {
const { columnName, parsedQuery, onQueryChange } = props;
- if (!parsedQuery.groupByExpression) return;
+ if (!parsedQuery.hasGroupBy()) return;
+ const ref = SqlRef.column(columnName);
- return (
- <MenuItem icon={IconNames.FUNCTION} text={`Aggregate`}>
+ function aggregateMenuItem(ex: SqlExpression, alias: string, run = true) {
+ return (
<MenuItem
- text={`COUNT(DISTINCT "${columnName}") AS "dist_${columnName}"`}
- onClick={() =>
- onQueryChange(
- parsedQuery.addAggregateColumn(
- [SqlRef.fromStringWithDoubleQuotes(columnName)],
- 'COUNT',
- `dist_${columnName}`,
- undefined,
- 'DISTINCT',
- ),
- true,
- )
- }
- />
- <MenuItem
- text={`COUNT(*) FILTER (WHERE "${columnName}" = 'xxx') AS
${columnName}_filtered_count `}
+ text={prettyPrintSql(ex)}
onClick={() => {
- onQueryChange(
- parsedQuery.addAggregateColumn(
- [SqlRef.fromString('*')],
- 'COUNT',
- `${columnName}_filtered_count`,
- SqlMulti.sqlMultiFactory('=', [
- SqlRef.fromStringWithDoubleQuotes(columnName),
- SqlLiteral.fromInput('xxx'),
- ]),
- ),
- );
+ onQueryChange(parsedQuery.addSelectExpression(ex.as(alias)), run);
}}
/>
+ );
+ }
+
+ return (
+ <MenuItem icon={IconNames.FUNCTION} text={`Aggregate`}>
+ {aggregateMenuItem(SqlFunction.decorated('COUNT', 'DISTINCT', [ref]),
`dist_${columnName}`)}
+ {aggregateMenuItem(
+ SqlFunction.simple('COUNT', [SqlRef.STAR], ref.equal(EMPTY_LITERAL)),
+ `filtered_dist_${columnName}`,
+ false,
+ )}
+ {aggregateMenuItem(
+ SqlFunction.simple('LATEST', [ref, SqlLiteral.create(100)]),
+ `latest_${columnName}`,
+ )}
</MenuItem>
);
}
@@ -171,65 +169,67 @@ export const StringMenuItems = React.memo(function
StringMenuItems(props: String
const { schema, table, columnName, parsedQuery, onQueryChange } = props;
if (schema !== 'lookup' || !parsedQuery) return;
- const { originalTableColumn, lookupColumn } =
getCurrentColumns(parsedQuery, table);
+ const { originalTableColumn, lookupColumn } = getJoinColumns(parsedQuery,
table);
return (
- <>
+ <MenuItem icon={IconNames.JOIN_TABLE} text={parsedQuery.hasJoin() ?
`Replace join` : `Join`}>
<MenuItem
- icon={IconNames.JOIN_TABLE}
- text={parsedQuery.joinTable ? `Replace join` : `Join`}
- >
- <MenuItem
- icon={IconNames.LEFT_JOIN}
- text={`Left join`}
- onClick={() => {
- onQueryChange(
- parsedQuery.addJoin(
+ icon={IconNames.LEFT_JOIN}
+ text={`Left join`}
+ onClick={() => {
+ onQueryChange(
+ parsedQuery.addJoin(
+ SqlJoinPart.create(
'LEFT',
- SqlRef.fromString(table, schema).upgrade(),
- SqlMulti.sqlMultiFactory('=', [
- SqlRef.fromString(columnName, table, 'lookup'),
- SqlRef.fromString(
+ SqlRef.column(table, schema).upgrade(),
+ SqlRef.column(columnName, table, 'lookup').equal(
+ SqlRef.column(
lookupColumn === columnName ? originalTableColumn :
'XXX',
- parsedQuery.getTableName(),
+ parsedQuery.getFirstTableName(),
),
- ]),
+ ),
),
- false,
- );
- }}
- />
- <MenuItem
- icon={IconNames.INNER_JOIN}
- text={`Inner join`}
- onClick={() => {
- onQueryChange(
- parsedQuery.addJoin(
+ ),
+ false,
+ );
+ }}
+ />
+ <MenuItem
+ icon={IconNames.INNER_JOIN}
+ text={`Inner join`}
+ onClick={() => {
+ onQueryChange(
+ parsedQuery.addJoin(
+ SqlJoinPart.create(
'INNER',
- SqlRef.fromString(table, schema).upgrade(),
- SqlMulti.sqlMultiFactory('=', [
- SqlRef.fromString(columnName, table, 'lookup'),
- SqlRef.fromString(
+ SqlRef.column(table, schema).upgrade(),
+ SqlRef.column(columnName, table, 'lookup').equal(
+ SqlRef.column(
lookupColumn === columnName ? originalTableColumn :
'XXX',
- parsedQuery.getTableName(),
+ parsedQuery.getFirstTableName(),
),
- ]),
+ ),
),
- false,
- );
- }}
- />
- </MenuItem>
- {parsedQuery.onExpression &&
- parsedQuery.onExpression instanceof SqlMulti &&
- parsedQuery.onExpression.containsColumn(columnName) && (
- <MenuItem
- icon={IconNames.EXCHANGE}
- text={`Remove join`}
- onClick={() => onQueryChange(parsedQuery.removeJoin())}
- />
- )}
- </>
+ ),
+ false,
+ );
+ }}
+ />
+ </MenuItem>
+ );
+ }
+
+ function renderRemoveJoin(): JSX.Element | undefined {
+ const { schema, parsedQuery, onQueryChange } = props;
+ if (schema !== 'lookup' || !parsedQuery) return;
+ if (!parsedQuery.hasJoin()) return;
+
+ return (
+ <MenuItem
+ icon={IconNames.EXCHANGE}
+ text={`Remove join`}
+ onClick={() => onQueryChange(parsedQuery.removeAllJoins())}
+ />
);
}
@@ -241,6 +241,7 @@ export const StringMenuItems = React.memo(function
StringMenuItems(props: String
{renderRemoveGroupBy()}
{renderAggregateMenu()}
{renderJoinMenu()}
+ {renderRemoveJoin()}
</>
);
});
diff --git
a/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.spec.tsx
b/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.spec.tsx
index 5858647..f05e0ce 100644
---
a/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.spec.tsx
+++
b/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.spec.tsx
@@ -17,7 +17,7 @@
*/
import { render } from '@testing-library/react';
-import { parseSqlQuery } from 'druid-query-toolkit';
+import { SqlQuery } from 'druid-query-toolkit';
import React from 'react';
import { TimeMenuItems } from './time-menu-items';
@@ -29,7 +29,7 @@ describe('time menu', () => {
table={'table'}
schema={'schema'}
columnName={'__time'}
- parsedQuery={parseSqlQuery(`SELECT channel, count(*) as cnt FROM
wikipedia GROUP BY 1`)}
+ parsedQuery={SqlQuery.parse(`SELECT channel, count(*) as cnt FROM
wikipedia GROUP BY 1`)}
onQueryChange={() => {}}
/>
);
@@ -44,7 +44,7 @@ describe('time menu', () => {
table={'table'}
schema={'schema'}
columnName={'__time'}
- parsedQuery={parseSqlQuery(`SELECT __time, count(*) as cnt FROM
wikipedia GROUP BY 1`)}
+ parsedQuery={SqlQuery.parse(`SELECT __time, count(*) as cnt FROM
wikipedia GROUP BY 1`)}
onQueryChange={() => {}}
/>
);
diff --git
a/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx
b/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx
index e2e7e18..a319577 100644
---
a/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx
+++
b/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx
@@ -18,30 +18,45 @@
import { MenuDivider, MenuItem } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
-import {
- SqlAliasRef,
- SqlFunction,
- SqlInterval,
- SqlLiteral,
- SqlMulti,
- SqlQuery,
- SqlRef,
- SqlTimestamp,
-} from 'druid-query-toolkit';
+import { SqlExpression, SqlFunction, SqlLiteral, SqlQuery, SqlRef } from
'druid-query-toolkit';
import React from 'react';
-import { getCurrentColumns } from '../../column-tree';
+import { prettyPrintSql } from '../../../../../utils';
-function dateToTimestamp(date: Date): SqlTimestamp {
- return SqlTimestamp.sqlTimestampFactory(
- date
- .toISOString()
- .split('.')[0]
- .split('T')
- .join(' '),
- );
+const LATEST_HOUR: SqlExpression = SqlExpression.parse(
+ `? >= CURRENT_TIMESTAMP - INTERVAL '1' HOUR`,
+);
+const LATEST_DAY: SqlExpression = SqlExpression.parse(`? >= CURRENT_TIMESTAMP
- INTERVAL '1' DAY`);
+const LATEST_WEEK: SqlExpression = SqlExpression.parse(
+ `? >= CURRENT_TIMESTAMP - INTERVAL '1' WEEK`,
+);
+const LATEST_MONTH: SqlExpression = SqlExpression.parse(
+ `? >= CURRENT_TIMESTAMP - INTERVAL '1' MONTH`,
+);
+const LATEST_YEAR: SqlExpression = SqlExpression.parse(
+ `? >= CURRENT_TIMESTAMP - INTERVAL '1' YEAR`,
+);
+
+const BETWEEN: SqlExpression = SqlExpression.parse(`(? <= ? AND ? < ?)`);
+
+// ------------------------------------
+
+function fillWithColumn(b: SqlExpression, columnName: string): SqlExpression {
+ return b.fillPlaceholders([SqlRef.column(columnName)]) as SqlExpression;
+}
+
+function fillWithColumnStartEnd(columnName: string, start: Date, end: Date):
SqlExpression {
+ const ref = SqlRef.column(columnName);
+ return BETWEEN.fillPlaceholders([
+ SqlLiteral.create(start),
+ ref,
+ ref,
+ SqlLiteral.create(end),
+ ]) as SqlExpression;
}
+// ------------------------------------
+
function floorHour(dt: Date): Date {
dt = new Date(dt.valueOf());
dt.setUTCMinutes(0, 0, 0);
@@ -97,205 +112,67 @@ export interface TimeMenuItemsProps {
schema: string;
columnName: string;
parsedQuery: SqlQuery;
- onQueryChange: (queryString: SqlQuery, run?: boolean) => void;
+ onQueryChange: (query: SqlQuery, run?: boolean) => void;
}
export const TimeMenuItems = React.memo(function TimeMenuItems(props:
TimeMenuItemsProps) {
function renderFilterMenu(): JSX.Element | undefined {
const { columnName, parsedQuery, onQueryChange } = props;
- const now = new Date();
- return (
- <MenuItem icon={IconNames.FILTER} text={`Filter`}>
- <MenuItem
- text={`Latest hour`}
- onClick={() => {
- onQueryChange(
- parsedQuery
- .removeFilter(columnName)
- .addWhereFilter(
- columnName,
- '>=',
- SqlMulti.sqlMultiFactory('-', [
- SqlRef.fromString('CURRENT_TIMESTAMP'),
- SqlInterval.sqlIntervalFactory('HOUR', 1),
- ]),
- ),
- true,
- );
- }}
- />
- <MenuItem
- text={`Latest day`}
- onClick={() => {
- onQueryChange(
- parsedQuery
- .removeFilter(columnName)
- .addWhereFilter(
- columnName,
- '>=',
- SqlMulti.sqlMultiFactory('-', [
- SqlRef.fromString('CURRENT_TIMESTAMP'),
- SqlInterval.sqlIntervalFactory('Day', 1),
- ]),
- ),
- true,
- );
- }}
- />
- <MenuItem
- text={`Latest week`}
- onClick={() => {
- onQueryChange(
- parsedQuery
- .removeFilter(columnName)
- .addWhereFilter(
- columnName,
- '>=',
- SqlMulti.sqlMultiFactory('-', [
- SqlRef.fromString('CURRENT_TIMESTAMP'),
- SqlInterval.sqlIntervalFactory('Day', 7),
- ]),
- ),
- true,
- );
- }}
- />
- <MenuItem
- text={`Latest month`}
- onClick={() => {
- onQueryChange(
- parsedQuery
- .removeFilter(columnName)
- .addWhereFilter(
- columnName,
- '>=',
- SqlMulti.sqlMultiFactory('-', [
- SqlRef.fromString('CURRENT_TIMESTAMP'),
- SqlInterval.sqlIntervalFactory('MONTH', 1),
- ]),
- ),
- true,
- );
- }}
- />
+ function filterMenuItem(label: string, clause: SqlExpression) {
+ return (
<MenuItem
- text={`Latest year`}
+ text={label}
onClick={() => {
- onQueryChange(
- parsedQuery
- .removeFilter(columnName)
- .addWhereFilter(
- columnName,
- '>=',
- SqlMulti.sqlMultiFactory('-', [
- SqlRef.fromString('CURRENT_TIMESTAMP'),
- SqlInterval.sqlIntervalFactory('YEAR', 1),
- ]),
- ),
- true,
- );
+
onQueryChange(parsedQuery.removeColumnFromWhere(columnName).addToWhere(clause),
true);
}}
/>
+ );
+ }
+
+ const now = new Date();
+ const hourStart = floorHour(now);
+ const dayStart = floorDay(now);
+ const monthStart = floorMonth(now);
+ const yearStart = floorYear(now);
+ return (
+ <MenuItem icon={IconNames.FILTER} text={`Filter`}>
+ {filterMenuItem(`Latest hour`, fillWithColumn(LATEST_HOUR,
columnName))}
+ {filterMenuItem(`Latest day`, fillWithColumn(LATEST_DAY, columnName))}
+ {filterMenuItem(`Latest week`, fillWithColumn(LATEST_WEEK,
columnName))}
+ {filterMenuItem(`Latest month`, fillWithColumn(LATEST_MONTH,
columnName))}
+ {filterMenuItem(`Latest year`, fillWithColumn(LATEST_YEAR,
columnName))}
<MenuDivider />
- <MenuItem
- text={`Current hour`}
- onClick={() => {
- const hourStart = floorHour(now);
- onQueryChange(
- parsedQuery
- .removeFilter(columnName)
- .addWhereFilter(
- SqlRef.fromStringWithDoubleQuotes(columnName),
- '>=',
- dateToTimestamp(hourStart),
- )
- .addWhereFilter(
- SqlRef.fromStringWithDoubleQuotes(columnName),
- '<',
- dateToTimestamp(nextHour(hourStart)),
- ),
- true,
- );
- }}
- />
- <MenuItem
- text={`Current day`}
- onClick={() => {
- const dayStart = floorDay(now);
- onQueryChange(
- parsedQuery
- .removeFilter(columnName)
- .addWhereFilter(
- SqlRef.fromStringWithDoubleQuotes(columnName),
- '>=',
- dateToTimestamp(dayStart),
- )
- .addWhereFilter(
- SqlRef.fromStringWithDoubleQuotes(columnName),
- '<',
- dateToTimestamp(nextDay(dayStart)),
- ),
- true,
- );
- }}
- />
- <MenuItem
- text={`Current month`}
- onClick={() => {
- const monthStart = floorMonth(now);
- onQueryChange(
- parsedQuery
- .removeFilter(columnName)
- .addWhereFilter(
- SqlRef.fromStringWithDoubleQuotes(columnName),
- '>=',
- dateToTimestamp(monthStart),
- )
- .addWhereFilter(
- SqlRef.fromStringWithDoubleQuotes(columnName),
- '<',
- dateToTimestamp(nextMonth(monthStart)),
- ),
- true,
- );
- }}
- />
- <MenuItem
- text={`Current year`}
- onClick={() => {
- const yearStart = floorYear(now);
- onQueryChange(
- parsedQuery
- .removeFilter(columnName)
- .addWhereFilter(
- SqlRef.fromStringWithDoubleQuotes(columnName),
- '<=',
- dateToTimestamp(yearStart),
- )
- .addWhereFilter(
- dateToTimestamp(yearStart),
- '<',
- dateToTimestamp(nextYear(yearStart)),
- ),
- true,
- );
- }}
- />
+ {filterMenuItem(
+ `Current hour`,
+ fillWithColumnStartEnd(columnName, hourStart, nextHour(hourStart)),
+ )}
+ {filterMenuItem(
+ `Current day`,
+ fillWithColumnStartEnd(columnName, dayStart, nextDay(dayStart)),
+ )}
+ {filterMenuItem(
+ `Current month`,
+ fillWithColumnStartEnd(columnName, monthStart,
nextMonth(monthStart)),
+ )}
+ {filterMenuItem(
+ `Current year`,
+ fillWithColumnStartEnd(columnName, yearStart, nextYear(yearStart)),
+ )}
</MenuItem>
);
}
function renderRemoveFilter(): JSX.Element | undefined {
const { columnName, parsedQuery, onQueryChange } = props;
- if (!parsedQuery.getCurrentFilters().includes(columnName)) return;
+ if (!parsedQuery.getEffectiveWhereExpression().containsColumn(columnName))
return;
return (
<MenuItem
icon={IconNames.FILTER_REMOVE}
text={`Remove filter`}
onClick={() => {
- onQueryChange(parsedQuery.removeFilter(columnName), true);
+ onQueryChange(parsedQuery.removeColumnFromWhere(columnName), true);
}}
/>
);
@@ -303,13 +180,15 @@ export const TimeMenuItems = React.memo(function
TimeMenuItems(props: TimeMenuIt
function renderRemoveGroupBy(): JSX.Element | undefined {
const { columnName, parsedQuery, onQueryChange } = props;
- if (!parsedQuery.hasGroupByColumn(columnName)) return;
+ const selectIndex = parsedQuery.getSelectIndexForColumn(columnName);
+ if (!parsedQuery.isGroupedSelectIndex(selectIndex)) return;
+
return (
<MenuItem
icon={IconNames.UNGROUP_OBJECTS}
text={'Remove group by'}
onClick={() => {
- onQueryChange(parsedQuery.removeFromGroupBy(columnName), true);
+ onQueryChange(parsedQuery.removeSelectIndex(selectIndex), true);
}}
/>
);
@@ -317,164 +196,80 @@ export const TimeMenuItems = React.memo(function
TimeMenuItems(props: TimeMenuIt
function renderGroupByMenu(): JSX.Element | undefined {
const { columnName, parsedQuery, onQueryChange } = props;
- if (!parsedQuery.groupByExpression) return;
+ if (!parsedQuery.hasGroupBy()) return;
+ const ref = SqlRef.column(columnName);
- return (
- <MenuItem icon={IconNames.GROUP_OBJECTS} text={`Group by`}>
- <MenuItem
- text={`TIME_FLOOR("${columnName}", 'PT1H') AS
"${columnName}_time_floor"`}
- onClick={() => {
- onQueryChange(
- parsedQuery.addToGroupBy(
- SqlAliasRef.sqlAliasFactory(
- SqlFunction.sqlFunctionFactory('TIME_FLOOR', [
- SqlRef.fromString(columnName),
- SqlLiteral.fromInput('PT1h'),
- ]),
- `${columnName}_time_floor`,
- ),
- ),
- true,
- );
- }}
- />
- <MenuItem
- text={`TIME_FLOOR("${columnName}", 'P1D') AS
"${columnName}_time_floor"`}
- onClick={() => {
- onQueryChange(
- parsedQuery.addToGroupBy(
- SqlAliasRef.sqlAliasFactory(
- SqlFunction.sqlFunctionFactory('TIME_FLOOR', [
- SqlRef.fromString(columnName),
- SqlLiteral.fromInput('P1D'),
- ]),
- `${columnName}_time_floor`,
- ),
- ),
- true,
- );
- }}
- />
+ function groupByMenuItem(ex: SqlExpression, alias: string) {
+ return (
<MenuItem
- text={`TIME_FLOOR("${columnName}", 'P7D') AS
"${columnName}_time_floor"`}
+ text={prettyPrintSql(ex)}
onClick={() => {
- onQueryChange(
- parsedQuery.addToGroupBy(
- SqlAliasRef.sqlAliasFactory(
- SqlFunction.sqlFunctionFactory('TIME_FLOOR', [
- SqlRef.fromString(columnName),
- SqlLiteral.fromInput('P7D'),
- ]),
- `${columnName}_time_floor`,
- ),
- ),
- true,
- );
+ onQueryChange(parsedQuery.addToGroupBy(ex.as(alias)), true);
}}
/>
+ );
+ }
+
+ return (
+ <MenuItem icon={IconNames.GROUP_OBJECTS} text={`Group by`}>
+ {groupByMenuItem(
+ SqlFunction.simple('TIME_FLOOR', [ref, SqlLiteral.create('PT1H')]),
+ `${columnName}_by_hour`,
+ )}
+ {groupByMenuItem(
+ SqlFunction.simple('TIME_FLOOR', [ref, SqlLiteral.create('P1D')]),
+ `${columnName}_by_day`,
+ )}
+ {groupByMenuItem(
+ SqlFunction.simple('TIME_FLOOR', [ref, SqlLiteral.create('P1M')]),
+ `${columnName}_by_month`,
+ )}
+ {groupByMenuItem(
+ SqlFunction.simple('TIME_FLOOR', [ref, SqlLiteral.create('P1Y')]),
+ `${columnName}_by_year`,
+ )}
+ <MenuDivider />
+ {groupByMenuItem(
+ SqlFunction.simple('TIME_EXTRACT', [ref, SqlLiteral.create('HOUR')]),
+ `hour_of_${columnName}`,
+ )}
+ {groupByMenuItem(
+ SqlFunction.simple('TIME_EXTRACT', [ref, SqlLiteral.create('DAY')]),
+ `day_of_${columnName}`,
+ )}
+ {groupByMenuItem(
+ SqlFunction.simple('TIME_EXTRACT', [ref,
SqlLiteral.create('MONTH')]),
+ `month_of_${columnName}`,
+ )}
+ {groupByMenuItem(
+ SqlFunction.simple('TIME_EXTRACT', [ref, SqlLiteral.create('YEAR')]),
+ `year_of_${columnName}`,
+ )}
</MenuItem>
);
}
function renderAggregateMenu(): JSX.Element | undefined {
const { columnName, parsedQuery, onQueryChange } = props;
- if (!parsedQuery.groupByExpression) return;
+ if (!parsedQuery.hasGroupBy()) return;
+ const ref = SqlRef.column(columnName);
- return (
- <MenuItem icon={IconNames.FUNCTION} text={`Aggregate`}>
- <MenuItem
- text={`MAX("${columnName}") AS "max_${columnName}"`}
- onClick={() => {
- onQueryChange(
- parsedQuery.addAggregateColumn(
- [SqlRef.fromStringWithDoubleQuotes(columnName)],
- 'MAX',
- `max_${columnName}`,
- ),
- true,
- );
- }}
- />
+ function aggregateMenuItem(ex: SqlExpression, alias: string) {
+ return (
<MenuItem
- text={`MIN("${columnName}") AS "min_${columnName}"`}
+ text={prettyPrintSql(ex)}
onClick={() => {
- onQueryChange(
- parsedQuery.addAggregateColumn(
- [SqlRef.fromStringWithDoubleQuotes(columnName)],
- 'MIN',
- `min_${columnName}`,
- ),
- true,
- );
+ onQueryChange(parsedQuery.addSelectExpression(ex.as(alias)), true);
}}
/>
- </MenuItem>
- );
- }
-
- function renderJoinMenu(): JSX.Element | undefined {
- const { schema, table, columnName, parsedQuery, onQueryChange } = props;
- if (schema !== 'lookup' || !parsedQuery) return;
-
- const { originalTableColumn, lookupColumn } =
getCurrentColumns(parsedQuery, table);
+ );
+ }
return (
- <>
- <MenuItem
- icon={IconNames.JOIN_TABLE}
- text={parsedQuery.joinTable ? `Replace join` : `Join`}
- >
- <MenuItem
- icon={IconNames.LEFT_JOIN}
- text={`Left join`}
- onClick={() => {
- onQueryChange(
- parsedQuery.addJoin(
- 'LEFT',
- SqlRef.fromString(table, schema).upgrade(),
- SqlMulti.sqlMultiFactory('=', [
- SqlRef.fromString(columnName, table, 'lookup'),
- SqlRef.fromString(
- lookupColumn === columnName ? originalTableColumn :
'XXX',
- parsedQuery.getTableName(),
- ),
- ]),
- ),
- false,
- );
- }}
- />
- <MenuItem
- icon={IconNames.INNER_JOIN}
- text={`Inner join`}
- onClick={() => {
- onQueryChange(
- parsedQuery.addJoin(
- 'INNER',
- SqlRef.fromString(table, schema).upgrade(),
- SqlMulti.sqlMultiFactory('=', [
- SqlRef.fromString(columnName, table, 'lookup'),
- SqlRef.fromString(
- lookupColumn === columnName ? originalTableColumn :
'XXX',
- parsedQuery.getTableName(),
- ),
- ]),
- ),
- false,
- );
- }}
- />
- </MenuItem>
- {parsedQuery.onExpression &&
- parsedQuery.onExpression instanceof SqlMulti &&
- parsedQuery.onExpression.containsColumn(columnName) && (
- <MenuItem
- icon={IconNames.EXCHANGE}
- text={`Remove join`}
- onClick={() => onQueryChange(parsedQuery.removeJoin())}
- />
- )}
- </>
+ <MenuItem icon={IconNames.FUNCTION} text={`Aggregate`}>
+ {aggregateMenuItem(SqlFunction.simple('MAX', [ref]),
`max_${columnName}`)}
+ {aggregateMenuItem(SqlFunction.simple('MIN', [ref]),
`min_${columnName}`)}
+ </MenuItem>
);
}
@@ -485,7 +280,6 @@ export const TimeMenuItems = React.memo(function
TimeMenuItems(props: TimeMenuIt
{renderGroupByMenu()}
{renderRemoveGroupBy()}
{renderAggregateMenu()}
- {renderJoinMenu()}
</>
);
});
diff --git a/web-console/src/views/query-view/column-tree/column-tree.spec.tsx
b/web-console/src/views/query-view/column-tree/column-tree.spec.tsx
index 5636c74..b228a63 100644
--- a/web-console/src/views/query-view/column-tree/column-tree.spec.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree.spec.tsx
@@ -17,7 +17,7 @@
*/
import { render } from '@testing-library/react';
-import { parseSqlQuery } from 'druid-query-toolkit';
+import { SqlQuery } from 'druid-query-toolkit';
import React from 'react';
import { ColumnMetadata } from '../../../utils/column-metadata';
@@ -29,7 +29,7 @@ describe('column tree', () => {
const columnTree = (
<ColumnTree
getParsedQuery={() => {
- return parseSqlQuery(`SELECT channel, count(*) as cnt FROM wikipedia
GROUP BY 1`);
+ return SqlQuery.parse(`SELECT channel, count(*) as cnt FROM
wikipedia GROUP BY 1`);
}}
defaultSchema="druid"
defaultTable="wikipedia"
@@ -62,7 +62,7 @@ describe('column tree', () => {
},
] as ColumnMetadata[]
}
- onQueryStringChange={() => {}}
+ onQueryChange={() => {}}
/>
);
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 d672b07..5bd4d13 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
@@ -27,93 +27,96 @@ import {
Tree,
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
-import { SqlMulti, SqlQuery, SqlRef } from 'druid-query-toolkit';
+import {
+ SqlAlias,
+ SqlComparison,
+ SqlExpression,
+ SqlFunction,
+ SqlJoinPart,
+ SqlQuery,
+ SqlRef,
+} from 'druid-query-toolkit';
import React, { ChangeEvent } from 'react';
import { Loader } from '../../../components';
import { Deferred } from '../../../components/deferred/deferred';
-import { copyAndAlert, escapeSqlIdentifier, groupBy } from '../../../utils';
+import { copyAndAlert, groupBy, prettyPrintSql } from '../../../utils';
import { ColumnMetadata } from '../../../utils/column-metadata';
import { NumberMenuItems, StringMenuItems, TimeMenuItems } from
'./column-tree-menu';
import './column-tree.scss';
-function handleTableClick(
- tableSchema: string,
- nodeData: ITreeNode,
- onQueryStringChange: (queryString: string, run: boolean) => void,
-): void {
- let columns: string[];
- if (nodeData.childNodes) {
- columns = nodeData.childNodes.map(child =>
escapeSqlIdentifier(String(child.label)));
- } else {
- columns = ['*'];
- }
- if (tableSchema === 'druid') {
- onQueryStringChange(
- `SELECT ${columns.join(', ')}
-FROM ${escapeSqlIdentifier(String(nodeData.label))}
-WHERE "__time" >= CURRENT_TIMESTAMP - INTERVAL '1' DAY`,
- true,
- );
- } else {
- onQueryStringChange(
- `SELECT ${columns.join(', ')}
-FROM ${tableSchema}.${nodeData.label}`,
- true,
- );
- }
+const LAST_DAY = SqlExpression.parse(`__time >= CURRENT_TIMESTAMP - INTERVAL
'1' DAY`);
+const COUNT_STAR = SqlFunction.COUNT_STAR.as('Count');
+
+const STRING_QUERY = SqlQuery.parse(`SELECT
+ ?
+FROM ?
+GROUP BY 1
+ORDER BY 2 DESC`);
+
+const TIME_QUERY = SqlQuery.parse(`SELECT
+ TIME_FLOOR(?, 'PT1H') AS "Time"
+FROM ?
+GROUP BY 1
+ORDER BY 1 ASC`);
+
+interface HandleColumnClickOptions {
+ columnSchema: string;
+ columnTable: string;
+ columnName: string;
+ columnType: string;
+ parsedQuery: SqlQuery | undefined;
+ onQueryChange: (query: SqlQuery, run: boolean) => void;
}
-function handleColumnClick(
- columnSchema: string,
- columnTable: string,
- nodeData: ITreeNode,
- onQueryStringChange: (queryString: string, run: boolean) => void,
-): void {
+function handleColumnClick(options: HandleColumnClickOptions): void {
+ const { columnSchema, columnTable, columnName, columnType, parsedQuery,
onQueryChange } = options;
+
+ let query: SqlQuery;
+ const columnRef = SqlRef.column(columnName);
if (columnSchema === 'druid') {
- if (nodeData.icon === IconNames.TIME) {
- 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`,
- true,
- );
+ if (columnType === 'TIMESTAMP') {
+ query = TIME_QUERY.fillPlaceholders([columnRef,
SqlRef.table(columnTable)]) as SqlQuery;
} else {
- onQueryStringChange(
- `SELECT
- "${nodeData.label}",
- COUNT(*) AS "Count"
-FROM ${escapeSqlIdentifier(columnTable)}
-WHERE "__time" >= CURRENT_TIMESTAMP - INTERVAL '1' DAY
-GROUP BY 1
-ORDER BY "Count" DESC`,
- true,
- );
+ query = STRING_QUERY.fillPlaceholders([columnRef,
SqlRef.table(columnTable)]) as SqlQuery;
}
} else {
- onQueryStringChange(
- `SELECT
- ${escapeSqlIdentifier(String(nodeData.label))},
- COUNT(*) AS "Count"
-FROM ${columnSchema}.${columnTable}
-GROUP BY 1
-ORDER BY "Count" DESC`,
- true,
- );
+ query = STRING_QUERY.fillPlaceholders([
+ columnRef,
+ SqlRef.table(columnTable, columnSchema),
+ ]) as SqlQuery;
}
+
+ let where: SqlExpression | undefined;
+ let aggregates: SqlAlias[] = [];
+ if (parsedQuery && parsedQuery.getFirstTableName() === columnTable) {
+ where = parsedQuery.getWhereExpression();
+ aggregates = parsedQuery.getAggregateSelectExpressions();
+ } else if (columnSchema === 'druid') {
+ where = LAST_DAY;
+ }
+ if (!aggregates.length) {
+ aggregates.push(COUNT_STAR);
+ }
+
+ let newSelectExpressions = query.selectExpressions;
+ for (const aggregate of aggregates) {
+ newSelectExpressions = newSelectExpressions.addLast(aggregate);
+ }
+
+ onQueryChange(
+
query.changeSelectExpressions(newSelectExpressions).changeWhereExpression(where),
+ true,
+ );
}
export interface ColumnTreeProps {
columnMetadataLoading: boolean;
columnMetadata?: readonly ColumnMetadata[];
getParsedQuery: () => SqlQuery | undefined;
- onQueryStringChange: (queryString: string | SqlQuery, run?: boolean) => void;
+ onQueryChange: (query: SqlQuery, run?: boolean) => void;
defaultSchema?: string;
defaultTable?: string;
}
@@ -125,48 +128,44 @@ export interface ColumnTreeState {
selectedTreeIndex: number;
}
-export function getCurrentColumns(parsedQuery: SqlQuery, table: string) {
- let lookupColumn;
- let originalTableColumn;
- if (
- parsedQuery.joinTable &&
- parsedQuery.joinTable.table === table &&
- parsedQuery.onExpression &&
- parsedQuery.onExpression instanceof SqlMulti
- ) {
- parsedQuery.onExpression.arguments.map(argument => {
- if (argument instanceof SqlRef) {
- if (argument.namespace === 'lookup') {
- lookupColumn = argument.column;
- } else {
- originalTableColumn = argument.column;
- }
+export function getJoinColumns(parsedQuery: SqlQuery, _table: string) {
+ let lookupColumn: string | undefined;
+ let originalTableColumn: string | undefined;
+ if (parsedQuery.fromClause && parsedQuery.fromClause.joinParts) {
+ const firstOnExpression =
parsedQuery.fromClause.joinParts.first().onExpression;
+ if (firstOnExpression instanceof SqlComparison && firstOnExpression.op ===
'=') {
+ const { lhs, rhs } = firstOnExpression;
+ if (lhs instanceof SqlRef && lhs.namespace === 'lookup') {
+ lookupColumn = lhs.column;
}
- });
+ if (rhs instanceof SqlRef) {
+ originalTableColumn = rhs.column;
+ }
+ }
}
return {
- lookupColumn: lookupColumn || 'XXX',
+ lookupColumn: lookupColumn || 'k',
originalTableColumn: originalTableColumn || 'XXX',
};
}
export class ColumnTree extends React.PureComponent<ColumnTreeProps,
ColumnTreeState> {
static getDerivedStateFromProps(props: ColumnTreeProps, state:
ColumnTreeState) {
- const { columnMetadata, defaultSchema, defaultTable } = props;
+ const { columnMetadata, defaultSchema, defaultTable, onQueryChange } =
props;
if (columnMetadata && columnMetadata !== state.prevColumnMetadata) {
const columnTree = groupBy(
columnMetadata,
r => r.TABLE_SCHEMA,
- (metadata, schema): ITreeNode => ({
- id: schema,
- label: schema,
+ (metadata, schemaName): ITreeNode => ({
+ id: schemaName,
+ label: schemaName,
childNodes: groupBy(
metadata,
r => r.TABLE_NAME,
- (metadata, table): ITreeNode => ({
- id: table,
+ (metadata, tableName): ITreeNode => ({
+ id: tableName,
icon: IconNames.TH,
label: (
<Popover
@@ -176,70 +175,77 @@ export class ColumnTree extends
React.PureComponent<ColumnTreeProps, ColumnTreeS
<Deferred
content={() => {
const parsedQuery = props.getParsedQuery();
+ const tableRef = SqlRef.table(tableName).as();
+ const prettyTableRef = prettyPrintSql(tableRef);
return (
<Menu>
<MenuItem
icon={IconNames.FULLSCREEN}
- text={`SELECT ... FROM ${table}`}
+ text={`SELECT ... FROM ${tableName}`}
onClick={() => {
- handleTableClick(
- schema,
- {
- id: table,
- icon: IconNames.TH,
- label: table,
- childNodes: metadata.map(columnData => ({
- id: columnData.COLUMN_NAME,
- icon:
ColumnTree.dataTypeToIcon(columnData.DATA_TYPE),
- label: columnData.COLUMN_NAME,
- })),
- },
- props.onQueryStringChange,
+ const tableRef = SqlRef.table(
+ tableName,
+ schemaName === 'druid' ? undefined :
schemaName,
+ );
+
+ let where: SqlExpression | undefined;
+ if (parsedQuery &&
parsedQuery.getFirstTableName() === tableName) {
+ where = parsedQuery.getWhereExpression();
+ } else if (schemaName === 'druid') {
+ where = LAST_DAY;
+ }
+
+ onQueryChange(
+ SqlQuery.create(tableRef)
+ .changeSelectExpressions(
+ metadata.map(child =>
SqlRef.column(child.COLUMN_NAME).as()),
+ )
+ .changeWhereExpression(where),
+ true,
);
}}
/>
- <MenuItem
- icon={IconNames.CLIPBOARD}
- text={`Copy: ${table}`}
- onClick={() => {
- copyAndAlert(table, `${table} query copied to
clipboard`);
- }}
- />
- {parsedQuery && (
+ {parsedQuery && parsedQuery.getFirstTableName()
!== tableName && (
<MenuItem
icon={IconNames.EXCHANGE}
- text={`Replace FROM with: ${table}`}
+ text={`Replace FROM with: ${prettyTableRef}`}
onClick={() => {
-
props.onQueryStringChange(parsedQuery.replaceFrom(table), true);
+ onQueryChange(
+
parsedQuery.changeFromExpressions([tableRef]),
+ true,
+ );
}}
/>
)}
- {parsedQuery && schema === 'lookup' && (
+ {parsedQuery && schemaName === 'lookup' && (
<MenuItem
popoverProps={{ openOnTargetFocus: false }}
icon={IconNames.JOIN_TABLE}
- text={parsedQuery.joinTable ? `Replace join` :
`Join`}
+ text={parsedQuery.hasJoin() ? `Replace join` :
`Join`}
>
<MenuItem
icon={IconNames.LEFT_JOIN}
text={`Left join`}
onClick={() => {
- const { lookupColumn, originalTableColumn
} = getCurrentColumns(
+ const { lookupColumn, originalTableColumn
} = getJoinColumns(
parsedQuery,
- table,
+ tableName,
);
- props.onQueryStringChange(
- parsedQuery.addJoin(
- 'LEFT',
- SqlRef.fromString(table,
schema).upgrade(),
- SqlMulti.sqlMultiFactory('=', [
- SqlRef.fromString(lookupColumn,
table, 'lookup'),
- SqlRef.fromString(
- originalTableColumn,
- parsedQuery.getTableName(),
+ onQueryChange(
+ parsedQuery
+ .removeAllJoins()
+ .addJoin(
+ SqlJoinPart.create(
+ 'LEFT',
+ SqlRef.column(tableName,
schemaName).upgrade(),
+ SqlRef.column(lookupColumn,
tableName, 'lookup').equal(
+ SqlRef.column(
+ originalTableColumn,
+
parsedQuery.getFirstTableName(),
+ ),
+ ),
),
- ]),
- ),
+ ),
false,
);
}}
@@ -248,21 +254,22 @@ export class ColumnTree extends
React.PureComponent<ColumnTreeProps, ColumnTreeS
icon={IconNames.INNER_JOIN}
text={`Inner join`}
onClick={() => {
- const { lookupColumn, originalTableColumn
} = getCurrentColumns(
+ const { lookupColumn, originalTableColumn
} = getJoinColumns(
parsedQuery,
- table,
+ tableName,
);
- props.onQueryStringChange(
+ onQueryChange(
parsedQuery.addJoin(
- 'INNER',
- SqlRef.fromString(table,
schema).upgrade(),
- SqlMulti.sqlMultiFactory('=', [
- SqlRef.fromString(lookupColumn,
table, 'lookup'),
- SqlRef.fromString(
- originalTableColumn,
- parsedQuery.getTableName(),
+ SqlJoinPart.create(
+ 'INNER',
+ SqlRef.column(tableName,
schemaName).upgrade(),
+ SqlRef.column(lookupColumn,
tableName, 'lookup').equal(
+ SqlRef.column(
+ originalTableColumn,
+ parsedQuery.getFirstTableName(),
+ ),
),
- ]),
+ ),
),
false,
);
@@ -271,23 +278,42 @@ export class ColumnTree extends
React.PureComponent<ColumnTreeProps, ColumnTreeS
</MenuItem>
)}
{parsedQuery &&
- parsedQuery.joinTable &&
- parsedQuery.joinTable.table === table && (
+ parsedQuery.hasJoin() &&
+ parsedQuery.getJoins()[0].table.toString() ===
tableName && (
<MenuItem
icon={IconNames.EXCHANGE}
text={`Remove join`}
+ onClick={() =>
onQueryChange(parsedQuery.removeAllJoins())}
+ />
+ )}
+ {parsedQuery &&
+ parsedQuery.hasGroupBy() &&
+ parsedQuery.getFirstTableName() === tableName &&
(
+ <MenuItem
+ icon={IconNames.FUNCTION}
+ text={`Aggregate COUNT(*)`}
onClick={() =>
-
props.onQueryStringChange(parsedQuery.removeJoin())
+
onQueryChange(parsedQuery.addSelectExpression(COUNT_STAR), true)
}
/>
)}
+ <MenuItem
+ icon={IconNames.CLIPBOARD}
+ text={`Copy: ${prettyTableRef}`}
+ onClick={() => {
+ copyAndAlert(
+ tableRef.toString(),
+ `${prettyTableRef} query copied to
clipboard`,
+ );
+ }}
+ />
</Menu>
);
}}
/>
}
>
- <div>{table}</div>
+ <div>{tableName}</div>
</Popover>
),
childNodes: metadata
@@ -311,45 +337,43 @@ export class ColumnTree extends
React.PureComponent<ColumnTreeProps, ColumnTreeS
icon={IconNames.FULLSCREEN}
text={`Show: ${columnData.COLUMN_NAME}`}
onClick={() => {
- handleColumnClick(
- schema,
- table,
- {
- id: columnData.COLUMN_NAME,
- icon:
ColumnTree.dataTypeToIcon(columnData.DATA_TYPE),
- label: columnData.COLUMN_NAME,
- },
- props.onQueryStringChange,
- );
+ handleColumnClick({
+ columnSchema: schemaName,
+ columnTable: tableName,
+ columnName: columnData.COLUMN_NAME,
+ columnType: columnData.DATA_TYPE,
+ parsedQuery,
+ onQueryChange: onQueryChange,
+ });
}}
/>
{parsedQuery &&
(columnData.DATA_TYPE === 'BIGINT' ||
columnData.DATA_TYPE === 'FLOAT') && (
<NumberMenuItems
- table={table}
- schema={schema}
+ table={tableName}
+ schema={schemaName}
columnName={columnData.COLUMN_NAME}
parsedQuery={parsedQuery}
-
onQueryChange={props.onQueryStringChange}
+ onQueryChange={onQueryChange}
/>
)}
{parsedQuery && columnData.DATA_TYPE ===
'VARCHAR' && (
<StringMenuItems
- table={table}
- schema={schema}
+ table={tableName}
+ schema={schemaName}
columnName={columnData.COLUMN_NAME}
parsedQuery={parsedQuery}
- onQueryChange={props.onQueryStringChange}
+ onQueryChange={onQueryChange}
/>
)}
{parsedQuery && columnData.DATA_TYPE ===
'TIMESTAMP' && (
<TimeMenuItems
- table={table}
- schema={schema}
+ table={tableName}
+ schema={schemaName}
columnName={columnData.COLUMN_NAME}
parsedQuery={parsedQuery}
- onQueryChange={props.onQueryStringChange}
+ onQueryChange={onQueryChange}
/>
)}
<MenuItem
diff --git
a/web-console/src/views/query-view/query-extra-info/__snapshots__/query-extra-info.spec.tsx.snap
b/web-console/src/views/query-view/query-extra-info/__snapshots__/query-extra-info.spec.tsx.snap
index 9984baa..310ea13 100644
---
a/web-console/src/views/query-view/query-extra-info/__snapshots__/query-extra-info.spec.tsx.snap
+++
b/web-console/src/views/query-view/query-extra-info/__snapshots__/query-extra-info.spec.tsx.snap
@@ -17,7 +17,7 @@ exports[`query extra info matches snapshot 1`] = `
class=""
tabindex="0"
>
- 999+ results in 8.00s
+ 0 results in 8.00s
</span>
</span>
</span>
diff --git
a/web-console/src/views/query-view/query-extra-info/query-extra-info.spec.tsx
b/web-console/src/views/query-view/query-extra-info/query-extra-info.spec.tsx
index 621942e..9c43073 100644
---
a/web-console/src/views/query-view/query-extra-info/query-extra-info.spec.tsx
+++
b/web-console/src/views/query-view/query-extra-info/query-extra-info.spec.tsx
@@ -17,6 +17,7 @@
*/
import { render } from '@testing-library/react';
+import { QueryResult } from 'druid-query-toolkit';
import React from 'react';
import { QueryExtraInfo } from './query-extra-info';
@@ -25,13 +26,9 @@ describe('query extra info', () => {
it('matches snapshot', () => {
const queryExtraInfo = (
<QueryExtraInfo
- queryExtraInfo={{
- 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: 1000,
- wrapQueryLimit: 1000,
- }}
+ queryResult={QueryResult.BLANK.attachQueryId(
+ 'e3ee781b-c0b6-4385-9d99-a8a1994bebac',
+ ).changeQueryDuration(8000)}
onDownload={() => {}}
/>
);
diff --git
a/web-console/src/views/query-view/query-extra-info/query-extra-info.tsx
b/web-console/src/views/query-view/query-extra-info/query-extra-info.tsx
index 7eae389..8a21546 100644
--- a/web-console/src/views/query-view/query-extra-info/query-extra-info.tsx
+++ b/web-console/src/views/query-view/query-extra-info/query-extra-info.tsx
@@ -28,6 +28,7 @@ import {
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import copy from 'copy-to-clipboard';
+import { QueryResult } from 'druid-query-toolkit';
import React from 'react';
import { AppToaster } from '../../../singletons/toaster';
@@ -35,25 +36,16 @@ import { pluralIfNeeded } from '../../../utils';
import './query-extra-info.scss';
-export interface QueryExtraInfoData {
- queryId?: string;
- sqlQueryId?: string;
- startTime: Date;
- endTime: Date;
- numResults: number;
- wrapQueryLimit: number | undefined;
-}
-
export interface QueryExtraInfoProps {
- queryExtraInfo: QueryExtraInfoData;
+ queryResult: QueryResult;
onDownload: (filename: string, format: string) => void;
}
export const QueryExtraInfo = React.memo(function QueryExtraInfo(props:
QueryExtraInfoProps) {
- const { queryExtraInfo, onDownload } = props;
+ const { queryResult, onDownload } = props;
function handleQueryInfoClick() {
- const id = queryExtraInfo.queryId || queryExtraInfo.sqlQueryId;
+ const id = queryResult.queryId || queryResult.sqlQueryId;
if (!id) return;
copy(id, { format: 'text/plain' });
@@ -64,7 +56,7 @@ export const QueryExtraInfo = React.memo(function
QueryExtraInfo(props: QueryExt
}
function handleDownload(format: string) {
- const id = queryExtraInfo.queryId || queryExtraInfo.sqlQueryId;
+ const id = queryResult.queryId || queryResult.sqlQueryId;
if (!id) return;
onDownload(`query-${id}.${format}`, format);
@@ -79,40 +71,38 @@ export const QueryExtraInfo = React.memo(function
QueryExtraInfo(props: QueryExt
</Menu>
);
+ const wrapQueryLimit = queryResult.getSqlOuterLimit();
let resultCount: string;
- if (
- queryExtraInfo.wrapQueryLimit &&
- queryExtraInfo.numResults === queryExtraInfo.wrapQueryLimit
- ) {
- resultCount = `${queryExtraInfo.numResults - 1}+ results`;
+ if (wrapQueryLimit && queryResult.getNumResults() === wrapQueryLimit) {
+ resultCount = `${queryResult.getNumResults() - 1}+ results`;
} else {
- resultCount = pluralIfNeeded(queryExtraInfo.numResults, 'result');
+ resultCount = pluralIfNeeded(queryResult.getNumResults(), 'result');
}
- const elapsed = queryExtraInfo.endTime.valueOf() -
queryExtraInfo.startTime.valueOf();
-
let tooltipContent: JSX.Element | undefined;
- if (queryExtraInfo.queryId) {
+ if (queryResult.queryId) {
tooltipContent = (
<>
- Query ID: <strong>{queryExtraInfo.queryId}</strong> (click to copy)
+ Query ID: <strong>{queryResult.queryId}</strong> (click to copy)
</>
);
- } else if (queryExtraInfo.sqlQueryId) {
+ } else if (queryResult.sqlQueryId) {
tooltipContent = (
<>
- SQL query ID: <strong>{queryExtraInfo.sqlQueryId}</strong> (click to
copy)
+ SQL query ID: <strong>{queryResult.sqlQueryId}</strong> (click to copy)
</>
);
}
return (
<div className="query-extra-info">
- <div className="query-info" onClick={handleQueryInfoClick}>
- <Tooltip content={tooltipContent} hoverOpenDelay={500}>
- {`${resultCount} in ${(elapsed / 1000).toFixed(2)}s`}
- </Tooltip>
- </div>
+ {typeof queryResult.queryDuration !== 'undefined' && (
+ <div className="query-info" onClick={handleQueryInfoClick}>
+ <Tooltip content={tooltipContent} hoverOpenDelay={500}>
+ {`${resultCount} in ${(queryResult.queryDuration /
1000).toFixed(2)}s`}
+ </Tooltip>
+ </div>
+ )}
<Popover className="download-button" content={downloadMenu}
position={Position.BOTTOM_RIGHT}>
<Button icon={IconNames.DOWNLOAD} minimal />
</Popover>
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 a8b058c..a72f6b8 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
@@ -17,7 +17,7 @@
*/
import { IResizeEntry, ResizeSensor } from '@blueprintjs/core';
-import ace from 'brace';
+import ace, { Editor } from 'brace';
import escape from 'lodash.escape';
import React from 'react';
import AceEditor from 'react-ace';
@@ -29,7 +29,7 @@ import {
SQL_KEYWORDS,
} from '../../../../lib/keywords';
import { SQL_DATA_TYPES, SQL_FUNCTIONS } from '../../../../lib/sql-docs';
-import { uniq } from '../../../utils';
+import { RowColumn, uniq } from '../../../utils';
import { ColumnMetadata } from '../../../utils/column-metadata';
import './query-input.scss';
@@ -56,6 +56,8 @@ export interface QueryInputState {
}
export class QueryInput extends React.PureComponent<QueryInputProps,
QueryInputState> {
+ private aceEditor: Editor | undefined;
+
static replaceDefaultAutoCompleter(): void {
if (!langTools) return;
@@ -209,6 +211,13 @@ export class QueryInput extends
React.PureComponent<QueryInputProps, QueryInputS
onQueryStringChange(value);
};
+ public goToRowColumn(rowColumn: RowColumn) {
+ const { aceEditor } = this;
+ if (!aceEditor) return;
+ aceEditor.getSelection().moveCursorTo(rowColumn.row, rowColumn.column);
+ aceEditor.focus(); // Grab the focus also
+ }
+
render(): JSX.Element {
const { queryString, runeMode } = this.props;
const { editorHeight } = this.state;
@@ -240,6 +249,9 @@ export class QueryInput extends
React.PureComponent<QueryInputProps, QueryInputS
}}
style={{}}
placeholder="SELECT * FROM ..."
+ onLoad={(editor: any) => {
+ this.aceEditor = editor;
+ }}
/>
</div>
</ResizeSensor>
diff --git
a/web-console/src/views/query-view/query-output/__snapshots__/query-output.spec.tsx.snap
b/web-console/src/views/query-view/query-output/__snapshots__/query-output.spec.tsx.snap
index 884542a..29d2ff2 100644
---
a/web-console/src/views/query-view/query-output/__snapshots__/query-output.spec.tsx.snap
+++
b/web-console/src/views/query-view/query-output/__snapshots__/query-output.spec.tsx.snap
@@ -38,6 +38,25 @@ exports[`query output matches snapshot 1`] = `
class=""
>
language
+ <span
+ class="bp3-icon bp3-icon-filter"
+ icon="filter"
+ >
+ <svg
+ data-icon="filter"
+ height="14"
+ viewBox="0 0 16 16"
+ width="14"
+ >
+ <desc>
+ filter
+ </desc>
+ <path
+ d="M13.99.99h-12a1.003 1.003 0 00-.71 1.71l4.71
4.71V14a1.003 1.003 0 001.71.71l2-2c.18-.18.29-.43.29-.71V7.41L14.7 2.7a1.003
1.003 0 00-.71-1.71z"
+ fill-rule="evenodd"
+ />
+ </svg>
+ </span>
</div>
</span>
</span>
@@ -65,6 +84,25 @@ exports[`query output matches snapshot 1`] = `
class=""
>
Count
+ <span
+ class="bp3-icon bp3-icon-filter"
+ icon="filter"
+ >
+ <svg
+ data-icon="filter"
+ height="14"
+ viewBox="0 0 16 16"
+ width="14"
+ >
+ <desc>
+ filter
+ </desc>
+ <path
+ d="M13.99.99h-12a1.003 1.003 0 00-.71 1.71l4.71
4.71V14a1.003 1.003 0 001.71.71l2-2c.18-.18.29-.43.29-.71V7.41L14.7 2.7a1.003
1.003 0 00-.71-1.71z"
+ fill-rule="evenodd"
+ />
+ </svg>
+ </span>
</div>
</span>
</span>
diff --git a/web-console/src/views/query-view/query-output/query-output.scss
b/web-console/src/views/query-view/query-output/query-output.scss
index c8807a9..e90d2eb 100644
--- a/web-console/src/views/query-view/query-output/query-output.scss
+++ b/web-console/src/views/query-view/query-output/query-output.scss
@@ -39,12 +39,18 @@
&.aggregate-header {
background: rgb(75, 122, 148);
}
+
.asc {
box-shadow: inset 0 3px 0 0 rgba(255, 255, 255, 0.6);
}
+
.desc {
box-shadow: inset 0 -3px 0 0 rgba(255, 255, 255, 0.6);
}
+
+ .bp3-icon {
+ margin-left: 3px;
+ }
}
.rt-td {
cursor: pointer;
diff --git
a/web-console/src/views/query-view/query-output/query-output.spec.tsx
b/web-console/src/views/query-view/query-output/query-output.spec.tsx
index 6417b91..33adf69 100644
--- a/web-console/src/views/query-view/query-output/query-output.spec.tsx
+++ b/web-console/src/views/query-view/query-output/query-output.spec.tsx
@@ -17,14 +17,14 @@
*/
import { render } from '@testing-library/react';
-import { parseSqlQuery } from 'druid-query-toolkit';
+import { QueryResult, SqlQuery } from 'druid-query-toolkit';
import React from 'react';
import { QueryOutput } from './query-output';
describe('query output', () => {
it('matches snapshot', () => {
- const parsedQuery = parseSqlQuery(`SELECT
+ const parsedQuery = SqlQuery.parse(`SELECT
"language",
COUNT(*) AS "Count", COUNT(DISTINCT "language") AS "dist_language", COUNT(*)
FILTER (WHERE "language"= 'xxx') AS "language_filtered_count"
FROM "github"
@@ -38,17 +38,18 @@ ORDER BY "Count" DESC`);
runeMode={false}
loading={false}
error="lol"
- queryResult={{
- header: ['language', 'Count', 'dist_language',
'language_filtered_count'],
- rows: [
+ queryResult={QueryResult.fromRawResult(
+ [
+ ['language', 'Count', 'dist_language', 'language_filtered_count'],
['', 6881, 1, 0],
['JavaScript', 166, 1, 0],
['Python', 62, 1, 0],
['HTML', 46, 1, 0],
[],
],
- }}
- parsedQuery={parsedQuery}
+ false,
+ true,
+ ).attachQuery({}, parsedQuery)}
onQueryChange={() => null}
/>
);
diff --git a/web-console/src/views/query-view/query-output/query-output.tsx
b/web-console/src/views/query-view/query-output/query-output.tsx
index c141c66..238fa6e 100644
--- a/web-console/src/views/query-view/query-output/query-output.tsx
+++ b/web-console/src/views/query-view/query-output/query-output.tsx
@@ -16,116 +16,163 @@
* limitations under the License.
*/
-import { Menu, MenuItem, Popover } from '@blueprintjs/core';
-import { IconNames } from '@blueprintjs/icons';
-import { HeaderRows, SqlQuery } from 'druid-query-toolkit';
-import { basicIdentifierEscape, basicLiteralEscape } from
'druid-query-toolkit/build/sql/helpers';
+import { Icon, Menu, MenuItem, Popover } from '@blueprintjs/core';
+import { IconName, IconNames } from '@blueprintjs/icons';
+import {
+ QueryResult,
+ SqlExpression,
+ SqlLiteral,
+ SqlQuery,
+ SqlRef,
+ trimString,
+} from 'druid-query-toolkit';
import React, { useState } from 'react';
import ReactTable from 'react-table';
import { TableCell } from '../../../components';
import { ShowValueDialog } from
'../../../dialogs/show-value-dialog/show-value-dialog';
-import { copyAndAlert } from '../../../utils';
+import { copyAndAlert, prettyPrintSql } from '../../../utils';
import { BasicAction, basicActionsToMenu } from '../../../utils/basic-action';
import './query-output.scss';
-function trimValue(str: any): string {
- str = String(str);
- if (str.length < 102) return str;
- return str.substr(0, 100) + '...';
+function isComparable(x: unknown): boolean {
+ return x !== null && x !== '' && !isNaN(Number(x));
}
export interface QueryOutputProps {
loading: boolean;
- queryResult?: HeaderRows;
- parsedQuery?: SqlQuery;
+ queryResult?: QueryResult;
onQueryChange: (query: SqlQuery, run?: boolean) => void;
error?: string;
runeMode: boolean;
}
export const QueryOutput = React.memo(function QueryOutput(props:
QueryOutputProps) {
- const { queryResult, parsedQuery, loading, error } = props;
+ const { queryResult, loading, error } = props;
+ const parsedQuery = queryResult ? queryResult.sqlQuery : undefined;
const [showValue, setShowValue] = useState();
- function getHeaderMenu(header: string) {
- const { parsedQuery, onQueryChange, runeMode } = props;
+ function hasFilterOnHeader(header: string, headerIndex: number): boolean {
+ if (!parsedQuery ||
!parsedQuery.isRealOutputColumnAtSelectIndex(headerIndex)) return false;
+
+ return (
+ parsedQuery.getEffectiveWhereExpression().containsColumn(header) ||
+ parsedQuery.getEffectiveHavingExpression().containsColumn(header)
+ );
+ }
+
+ function getHeaderMenu(header: string, headerIndex: number) {
+ const { onQueryChange, runeMode } = props;
+ const ref = SqlRef.column(header);
+ const prettyRef = prettyPrintSql(ref);
if (parsedQuery) {
- const sorted = parsedQuery.getSorted();
+ const orderByExpression = parsedQuery.isValidSelectIndex(headerIndex)
+ ? SqlLiteral.index(headerIndex)
+ : SqlRef.column(header);
+ const descOrderBy = orderByExpression.toOrderByPart('DESC');
+ const ascOrderBy = orderByExpression.toOrderByPart('ASC');
+ const orderBy = parsedQuery.getOrderByForSelectIndex(headerIndex);
const basicActions: BasicAction[] = [];
- if (sorted) {
- sorted.map(sorted => {
- if (sorted.id === header) {
- basicActions.push({
- icon: sorted.desc ? IconNames.SORT_ASC : IconNames.SORT_DESC,
- title: `Order by: ${trimValue(header)} ${sorted.desc ? 'ASC' :
'DESC'}`,
- onAction: () => {
- onQueryChange(parsedQuery.orderBy(header, sorted.desc ? 'ASC'
: 'DESC'), true);
- },
- });
- }
+ if (orderBy) {
+ const reverseOrderBy = orderBy.reverseDirection();
+ const reverseOrderByDirection = reverseOrderBy.getEffectiveDirection();
+ basicActions.push({
+ icon: reverseOrderByDirection === 'ASC' ? IconNames.SORT_ASC :
IconNames.SORT_DESC,
+ title: `Order ${reverseOrderByDirection === 'ASC' ? 'ascending' :
'descending'}`,
+ onAction: () => {
+
onQueryChange(parsedQuery.changeOrderByExpressions([reverseOrderBy]), true);
+ },
});
- }
- if (!basicActions.length) {
+ } else {
basicActions.push(
{
icon: IconNames.SORT_DESC,
- title: `Order by: ${trimValue(header)} DESC`,
+ title: `Order descending`,
onAction: () => {
- onQueryChange(parsedQuery.orderBy(header, 'DESC'), true);
+
onQueryChange(parsedQuery.changeOrderByExpressions([descOrderBy]), true);
},
},
{
icon: IconNames.SORT_ASC,
- title: `Order by: ${trimValue(header)} ASC`,
+ title: `Order ascending`,
onAction: () => {
- onQueryChange(parsedQuery.orderBy(header, 'ASC'), true);
+
onQueryChange(parsedQuery.changeOrderByExpressions([ascOrderBy]), true);
},
},
);
}
+
+ if (parsedQuery.isRealOutputColumnAtSelectIndex(headerIndex)) {
+ const whereExpression = parsedQuery.getWhereExpression();
+ if (whereExpression && whereExpression.containsColumn(header)) {
+ basicActions.push({
+ icon: IconNames.FILTER_REMOVE,
+ title: `Remove from WHERE clause`,
+ onAction: () => {
+ onQueryChange(
+
parsedQuery.changeWhereExpression(whereExpression.removeColumnFromAnd(header)),
+ true,
+ );
+ },
+ });
+ }
+
+ const havingExpression = parsedQuery.getHavingExpression();
+ if (havingExpression && havingExpression.containsColumn(header)) {
+ basicActions.push({
+ icon: IconNames.FILTER_REMOVE,
+ title: `Remove from HAVING clause`,
+ onAction: () => {
+ onQueryChange(
+
parsedQuery.changeHavingExpression(havingExpression.removeColumnFromAnd(header)),
+ true,
+ );
+ },
+ });
+ }
+ }
+
basicActions.push({
icon: IconNames.CROSS,
- title: `Remove: ${trimValue(header)}`,
+ title: `Remove column`,
onAction: () => {
- onQueryChange(parsedQuery.remove(header), true);
+ onQueryChange(parsedQuery.removeOutputColumn(header), true);
},
});
return basicActionsToMenu(basicActions);
} else {
+ const orderByExpression = SqlRef.column(header);
+ const descOrderBy = orderByExpression.toOrderByPart('DESC');
+ const ascOrderBy = orderByExpression.toOrderByPart('ASC');
+ const descOrderByPretty = prettyPrintSql(descOrderBy);
+ const ascOrderByPretty = prettyPrintSql(descOrderBy);
return (
<Menu>
<MenuItem
icon={IconNames.CLIPBOARD}
- text={`Copy: ${trimValue(header)}`}
+ text={`Copy: ${prettyRef}`}
onClick={() => {
- copyAndAlert(header, `${header}' copied to clipboard`);
+ copyAndAlert(String(ref), `${prettyRef}' copied to clipboard`);
}}
/>
{!runeMode && (
<>
<MenuItem
icon={IconNames.CLIPBOARD}
- text={`Copy: ORDER BY ${basicIdentifierEscape(header)} ASC`}
+ text={`Copy: ${descOrderByPretty}`}
onClick={() =>
- copyAndAlert(
- `ORDER BY ${basicIdentifierEscape(header)} ASC`,
- `ORDER BY ${basicIdentifierEscape(header)} ASC' copied to
clipboard`,
- )
+ copyAndAlert(descOrderBy.toString(), `'${descOrderByPretty}'
copied to clipboard`)
}
/>
<MenuItem
icon={IconNames.CLIPBOARD}
- text={`Copy: 'ORDER BY ${basicIdentifierEscape(header)} DESC'`}
+ text={`Copy: ${ascOrderByPretty}`}
onClick={() =>
- copyAndAlert(
- `ORDER BY ${basicIdentifierEscape(header)} DESC`,
- `ORDER BY ${basicIdentifierEscape(header)} DESC' copied to
clipboard`,
- )
+ copyAndAlert(ascOrderBy.toString(), `'${ascOrderByPretty}'
copied to clipboard`)
}
/>
</>
@@ -135,8 +182,37 @@ export const QueryOutput = React.memo(function
QueryOutput(props: QueryOutputPro
}
}
- function getCellMenu(header: string, value: any) {
- const { parsedQuery, onQueryChange, runeMode } = props;
+ function filterOnMenuItem(icon: IconName, clause: SqlExpression, having:
boolean) {
+ const { onQueryChange } = props;
+ if (!parsedQuery) return;
+
+ return (
+ <MenuItem
+ icon={icon}
+ text={`${having ? 'Having' : 'Filter on'}: ${prettyPrintSql(clause)}`}
+ onClick={() => {
+ onQueryChange(
+ having ? parsedQuery.addToHaving(clause) :
parsedQuery.addToWhere(clause),
+ true,
+ );
+ }}
+ />
+ );
+ }
+
+ function clipboardMenuItem(clause: SqlExpression) {
+ const prettyLabel = prettyPrintSql(clause);
+ return (
+ <MenuItem
+ icon={IconNames.CLIPBOARD}
+ text={`Copy: ${prettyLabel}`}
+ onClick={() => copyAndAlert(clause.toString(), `${prettyLabel} copied
to clipboard`)}
+ />
+ );
+ }
+
+ function getCellMenu(header: string, headerIndex: number, value: any) {
+ const { runeMode } = props;
const showFullValueMenuItem =
typeof value === 'string' ? (
@@ -151,117 +227,75 @@ export const QueryOutput = React.memo(function
QueryOutput(props: QueryOutputPro
undefined
);
+ const val = SqlLiteral.create(value);
if (parsedQuery) {
- return (
- <Menu>
- <MenuItem
- icon={IconNames.FILTER_KEEP}
- text={`Filter by: ${trimValue(header)} = ${trimValue(value)}`}
- onClick={() => {
- onQueryChange(parsedQuery.addWhereFilter(header, '=', value),
true);
- }}
- />
- <MenuItem
- icon={IconNames.FILTER_REMOVE}
- text={`Filter by: ${trimValue(header)} != ${trimValue(value)}`}
- onClick={() => {
- onQueryChange(parsedQuery.addWhereFilter(header, '!=', value),
true);
- }}
- />
- {!isNaN(Number(value)) && (
- <>
- <MenuItem
- icon={IconNames.FILTER_KEEP}
- text={`Filter by: ${trimValue(header)} >= ${trimValue(value)}`}
- onClick={() => {
- onQueryChange(parsedQuery.addWhereFilter(header, '>=',
value), true);
- }}
- />
- <MenuItem
- icon={IconNames.FILTER_KEEP}
- text={`Filter by: ${trimValue(header)} <= ${trimValue(value)}`}
- onClick={() => {
- onQueryChange(parsedQuery.addWhereFilter(header, '<=',
value), true);
- }}
- />
- </>
- )}
- {showFullValueMenuItem}
- </Menu>
- );
- } else {
- return (
- <Menu>
- <MenuItem
- icon={IconNames.CLIPBOARD}
- text={`Copy: ${trimValue(value)}`}
- onClick={() => copyAndAlert(value, `${value} copied to clipboard`)}
- />
- {!runeMode && (
- <>
- <MenuItem
- icon={IconNames.CLIPBOARD}
- text={`Copy: ${basicIdentifierEscape(header)} =
${basicLiteralEscape(value)}`}
- onClick={() =>
- copyAndAlert(
- `${basicIdentifierEscape(header)} =
${basicLiteralEscape(value)}`,
- `${basicIdentifierEscape(header)} = ${basicLiteralEscape(
- value,
- )} copied to clipboard`,
- )
- }
- />
- <MenuItem
- icon={IconNames.CLIPBOARD}
- text={`Copy: ${basicIdentifierEscape(header)} !=
${basicLiteralEscape(value)}`}
- onClick={() =>
- copyAndAlert(
- `${basicIdentifierEscape(header)} !=
${basicLiteralEscape(value)}`,
- `${basicIdentifierEscape(header)} != ${basicLiteralEscape(
- value,
- )} copied to clipboard`,
- )
- }
- />
- </>
- )}
- {showFullValueMenuItem}
- </Menu>
- );
+ const selectValue = parsedQuery.getSelectExpressionForIndex(headerIndex);
+ if (selectValue) {
+ const outputName = selectValue.getOutputName();
+ const having = parsedQuery.isAggregateSelectIndex(headerIndex);
+ let ex: SqlExpression;
+ if (having && outputName) {
+ ex = SqlRef.column(outputName);
+ } else {
+ ex = selectValue.expression as SqlExpression;
+ }
+
+ return (
+ <Menu>
+ {isComparable(value) && (
+ <>
+ {filterOnMenuItem(IconNames.FILTER_KEEP,
ex.greaterThanOrEqual(val), having)}
+ {filterOnMenuItem(IconNames.FILTER_KEEP,
ex.lessThanOrEqual(val), having)}
+ </>
+ )}
+ {filterOnMenuItem(IconNames.FILTER_KEEP, ex.equal(val), having)}
+ {filterOnMenuItem(IconNames.FILTER_REMOVE, ex.unequal(val),
having)}
+ {showFullValueMenuItem}
+ </Menu>
+ );
+ }
}
+
+ const ref = SqlRef.column(header);
+ const trimmedValue = trimString(String(value), 50);
+ return (
+ <Menu>
+ <MenuItem
+ icon={IconNames.CLIPBOARD}
+ text={`Copy: ${trimmedValue}`}
+ onClick={() => copyAndAlert(value, `${trimmedValue} copied to
clipboard`)}
+ />
+ {!runeMode && (
+ <>
+ {clipboardMenuItem(ref.equal(val))}
+ {clipboardMenuItem(ref.unequal(val))}
+ </>
+ )}
+ {showFullValueMenuItem}
+ </Menu>
+ );
}
function getHeaderClassName(header: string) {
- const { parsedQuery } = props;
if (!parsedQuery) return;
const className = [];
- const sorted = parsedQuery.getSorted();
- const aggregateColumns = parsedQuery.getAggregateColumns();
-
- if (sorted) {
- const sortedColumnNames = sorted.map(column => column.id);
- if (sortedColumnNames.includes(header)) {
- className.push(sorted[sortedColumnNames.indexOf(header)].desc ?
'-sort-desc' : '-sort-asc');
- }
+ const orderBy = parsedQuery.getOrderByForOutputColumn(header);
+ if (orderBy) {
+ className.push(orderBy.getEffectiveDirection() === 'DESC' ? '-sort-desc'
: '-sort-asc');
}
- if (aggregateColumns && aggregateColumns.includes(header)) {
+ if (parsedQuery.isAggregateOutputColumn(header)) {
className.push('aggregate-header');
}
return className.join(' ');
}
- let aggregateColumns: string[] | undefined;
- if (parsedQuery) {
- aggregateColumns = parsedQuery.getAggregateColumns();
- }
-
return (
<div className="query-output">
<ReactTable
- data={queryResult ? queryResult.rows : []}
+ data={queryResult ? (queryResult.rows as any[][]) : []}
loading={loading}
noDataText={
!loading && queryResult && !queryResult.rows.length
@@ -269,12 +303,16 @@ export const QueryOutput = React.memo(function
QueryOutput(props: QueryOutputPro
: error || ''
}
sortable={false}
- columns={(queryResult ? queryResult.header : []).map((h: any, i) => {
+ columns={(queryResult ? queryResult.header : []).map((column, i) => {
+ const h = column.name;
return {
Header: () => {
return (
- <Popover className={'clickable-cell'}
content={getHeaderMenu(h)}>
- <div>{h}</div>
+ <Popover className={'clickable-cell'}
content={getHeaderMenu(h, i)}>
+ <div>
+ {h}
+ {hasFilterOnHeader(h, i) && <Icon icon={IconNames.FILTER}
iconSize={14} />}
+ </div>
</Popover>
);
},
@@ -284,14 +322,16 @@ export const QueryOutput = React.memo(function
QueryOutput(props: QueryOutputPro
const value = row.value;
return (
<div>
- <Popover content={getCellMenu(h, value)}>
+ <Popover content={getCellMenu(h, i, value)}>
<TableCell value={value} unlimited />
</Popover>
</div>
);
},
className:
- aggregateColumns && aggregateColumns.includes(h) ?
'aggregate-column' : undefined,
+ parsedQuery && parsedQuery.isAggregateOutputColumn(h)
+ ? 'aggregate-column'
+ : undefined,
};
})}
/>
diff --git a/web-console/src/views/query-view/query-view.tsx
b/web-console/src/views/query-view/query-view.tsx
index 4e9a18c..d5a531a 100644
--- a/web-console/src/views/query-view/query-view.tsx
+++ b/web-console/src/views/query-view/query-view.tsx
@@ -19,17 +19,10 @@
import { Intent, Switch, Tooltip } from '@blueprintjs/core';
import axios from 'axios';
import classNames from 'classnames';
-import {
- HeaderRows,
- isFirstRowHeader,
- normalizeQueryResult,
- parseSqlQuery,
- shouldIncludeTimestamp,
- SqlQuery,
-} from 'druid-query-toolkit';
+import { QueryResult, QueryRunner, SqlQuery } from 'druid-query-toolkit';
import Hjson from 'hjson';
import memoizeOne from 'memoize-one';
-import React from 'react';
+import React, { RefObject } from 'react';
import SplitterLayout from 'react-splitter-layout';
import { QueryPlanDialog } from '../../dialogs';
@@ -39,6 +32,7 @@ import { AppToaster } from '../../singletons/toaster';
import {
BasicQueryExplanation,
downloadFile,
+ findEmptyLiteralPosition,
getDruidErrorMessage,
localStorageGet,
localStorageGetJson,
@@ -55,7 +49,7 @@ import { isEmptyContext, QueryContext } from
'../../utils/query-context';
import { QueryRecord, QueryRecordUtil } from '../../utils/query-history';
import { ColumnTree } from './column-tree/column-tree';
-import { QueryExtraInfo, QueryExtraInfoData } from
'./query-extra-info/query-extra-info';
+import { QueryExtraInfo } from './query-extra-info/query-extra-info';
import { QueryInput } from './query-input/query-input';
import { QueryOutput } from './query-output/query-output';
import { RunButton } from './run-button/run-button';
@@ -64,7 +58,7 @@ import './query-view.scss';
const parser = memoizeOne((sql: string): SqlQuery | undefined => {
try {
- return parseSqlQuery(sql);
+ return SqlQuery.parse(sql);
} catch {
return;
}
@@ -94,7 +88,7 @@ export interface QueryViewState {
columnMetadataError?: string;
loading: boolean;
- result?: QueryResult;
+ queryResult?: QueryResult;
error?: string;
explainDialogOpen: boolean;
@@ -110,12 +104,6 @@ export interface QueryViewState {
queryHistory: readonly QueryRecord[];
}
-interface QueryResult {
- queryResult: HeaderRows;
- queryExtraInfo: QueryExtraInfoData;
- parsedQuery?: SqlQuery;
-}
-
export class QueryView extends React.PureComponent<QueryViewProps,
QueryViewState> {
static trimSemicolon(query: string): string {
// Trims out a trailing semicolon while preserving space
(https://bit.ly/1n1yfkJ)
@@ -166,16 +154,20 @@ export class QueryView extends
React.PureComponent<QueryViewProps, QueryViewStat
}
private metadataQueryManager: QueryManager<null, ColumnMetadata[]>;
- private sqlQueryManager: QueryManager<QueryWithContext, QueryResult>;
+ private queryManager: QueryManager<QueryWithContext, QueryResult>;
private explainQueryManager: QueryManager<
QueryWithContext,
BasicQueryExplanation | SemiJoinQueryExplanation | string
>;
+ private queryInputRef: RefObject<QueryInput>;
+
constructor(props: QueryViewProps, context: any) {
super(props, context);
const { mandatoryQueryContext } = props;
+ this.queryInputRef = React.createRef();
+
const queryString = props.initQuery ||
localStorageGet(LocalStorageKeys.QUERY_KEY) || '';
const parsedQuery = queryString ? parser(queryString) : undefined;
@@ -228,86 +220,33 @@ export class QueryView extends
React.PureComponent<QueryViewProps, QueryViewStat
},
});
- this.sqlQueryManager = new QueryManager({
+ const queryRunner = new QueryRunner((payload, isSql) => {
+ return axios.post(`/druid/v2${isSql ? '/sql' : ''}`, payload);
+ });
+
+ this.queryManager = new QueryManager({
processQuery: async (queryWithContext: QueryWithContext):
Promise<QueryResult> => {
const { queryString, queryContext, wrapQueryLimit } = queryWithContext;
- let parsedQuery: SqlQuery | undefined;
- let jsonQuery: any;
-
- try {
- parsedQuery = parser(queryString);
- } catch {}
-
- if (!(parsedQuery instanceof SqlQuery)) {
- parsedQuery = undefined;
- }
- if (QueryView.isJsonLike(queryString)) {
- jsonQuery = Hjson.parse(queryString);
- } else {
- jsonQuery = {
- query: queryString,
- resultFormat: 'array',
- header: true,
- };
- }
+ const query = QueryView.isJsonLike(queryString) ?
Hjson.parse(queryString) : queryString;
+ let context: Record<string, any> | undefined;
if (!isEmptyContext(queryContext) || wrapQueryLimit ||
mandatoryQueryContext) {
- jsonQuery.context = Object.assign(
- {},
- jsonQuery.context || {},
- queryContext,
- mandatoryQueryContext || {},
- );
- jsonQuery.context.sqlOuterLimit = wrapQueryLimit;
- }
-
- let rawQueryResult: unknown;
- let queryId: string | undefined;
- let sqlQueryId: string | undefined;
- const startTime = new Date();
- let endTime: Date;
- if (!jsonQuery.queryType && typeof jsonQuery.query === 'string') {
- try {
- const sqlResultResp = await axios.post('/druid/v2/sql', jsonQuery);
- endTime = new Date();
- rawQueryResult = sqlResultResp.data;
- sqlQueryId = sqlResultResp.headers['x-druid-sql-query-id'];
- } catch (e) {
- throw new Error(getDruidErrorMessage(e));
- }
- } else {
- try {
- const runeResultResp = await axios.post('/druid/v2', jsonQuery);
- endTime = new Date();
- rawQueryResult = runeResultResp.data;
- queryId = runeResultResp.headers['x-druid-query-id'];
- } catch (e) {
- throw new Error(getDruidErrorMessage(e));
+ context = Object.assign({}, queryContext, mandatoryQueryContext ||
{});
+ if (typeof wrapQueryLimit !== 'undefined') {
+ context.sqlOuterLimit = wrapQueryLimit;
}
}
- const queryResult = normalizeQueryResult(
- rawQueryResult,
- shouldIncludeTimestamp(jsonQuery),
- isFirstRowHeader(jsonQuery),
- );
- return {
- queryResult,
- queryExtraInfo: {
- queryId,
- sqlQueryId,
- startTime,
- endTime,
- numResults: queryResult.rows.length,
- wrapQueryLimit,
- },
- parsedQuery,
- };
+ try {
+ return await queryRunner.runQuery(query, context);
+ } catch (e) {
+ throw new Error(getDruidErrorMessage(e));
+ }
},
onStateChange: ({ result, loading, error }) => {
this.setState({
- result,
+ queryResult: result,
loading,
error,
});
@@ -323,9 +262,15 @@ export class QueryView extends
React.PureComponent<QueryViewProps, QueryViewStat
resultFormat: 'object',
};
- if (!isEmptyContext(queryContext) || wrapQueryLimit) {
- explainPayload.context = Object.assign({}, queryContext || {});
- explainPayload.context.sqlOuterLimit = wrapQueryLimit;
+ if (!isEmptyContext(queryContext) || wrapQueryLimit ||
mandatoryQueryContext) {
+ explainPayload.context = Object.assign(
+ {},
+ queryContext || {},
+ mandatoryQueryContext || {},
+ );
+ if (typeof wrapQueryLimit !== 'undefined') {
+ explainPayload.context.sqlOuterLimit = wrapQueryLimit;
+ }
}
const result = await queryDruidSql(explainPayload);
@@ -347,27 +292,36 @@ export class QueryView extends
React.PureComponent<QueryViewProps, QueryViewStat
componentWillUnmount(): void {
this.metadataQueryManager.terminate();
- this.sqlQueryManager.terminate();
+ this.queryManager.terminate();
this.explainQueryManager.terminate();
}
prettyPrintJson(): void {
- this.setState(prevState => ({
- queryString: JSON.stringify(Hjson.parse(prevState.queryString), null, 2),
- }));
+ this.setState(prevState => {
+ let parsed: any;
+ try {
+ parsed = Hjson.parse(prevState.queryString);
+ } catch {
+ return null;
+ }
+ return {
+ queryString: JSON.stringify(parsed, null, 2),
+ };
+ });
}
handleDownload = (filename: string, format: string) => {
- const { result } = this.state;
- if (!result) return;
- const { queryResult } = result;
+ const { queryResult } = this.state;
+ if (!queryResult) return;
let lines: string[] = [];
let separator: string = '';
if (format === 'csv' || format === 'tsv') {
separator = format === 'csv' ? ',' : '\t';
- lines.push(queryResult.header.map(str => QueryView.formatStr(str,
format)).join(separator));
+ lines.push(
+ queryResult.header.map(column => QueryView.formatStr(column.name,
format)).join(separator),
+ );
lines = lines.concat(
queryResult.rows.map(r => r.map(cell => QueryView.formatStr(cell,
format)).join(separator)),
);
@@ -378,7 +332,7 @@ export class QueryView extends
React.PureComponent<QueryViewProps, QueryViewStat
for (let k = 0; k < r.length; k++) {
const newName = queryResult.header[k];
if (newName) {
- outputObject[newName] = r[k];
+ outputObject[newName.name] = r[k];
}
}
return JSON.stringify(outputObject);
@@ -473,15 +427,15 @@ export class QueryView extends
React.PureComponent<QueryViewProps, QueryViewStat
}
renderMainArea() {
- const { queryString, queryContext, loading, result, error, columnMetadata
} = this.state;
+ const { queryString, queryContext, loading, queryResult, error,
columnMetadata } = this.state;
const emptyQuery = QueryView.isEmptyQuery(queryString);
let currentSchema: string | undefined;
let currentTable: string | undefined;
- if (result && result.parsedQuery) {
- currentSchema = result.parsedQuery.getSchema();
- currentTable = result.parsedQuery.getTableName();
+ if (queryResult && queryResult.sqlQuery) {
+ currentSchema = queryResult.sqlQuery.getFirstSchema();
+ currentTable = queryResult.sqlQuery.getFirstTableName();
} else if (localStorageGet(LocalStorageKeys.QUERY_KEY)) {
const defaultQueryString = localStorageGet(LocalStorageKeys.QUERY_KEY);
@@ -490,8 +444,8 @@ export class QueryView extends
React.PureComponent<QueryViewProps, QueryViewStat
: undefined;
if (defaultQueryAst) {
- currentSchema = defaultQueryAst.getSchema();
- currentTable = defaultQueryAst.getTableName();
+ currentSchema = defaultQueryAst.getFirstSchema();
+ currentTable = defaultQueryAst.getFirstTableName();
}
}
@@ -509,6 +463,7 @@ export class QueryView extends
React.PureComponent<QueryViewProps, QueryViewStat
>
<div className="control-pane">
<QueryInput
+ ref={this.queryInputRef}
currentSchema={currentSchema ? currentSchema : 'druid'}
currentTable={currentTable}
queryString={queryString}
@@ -530,11 +485,8 @@ export class QueryView extends
React.PureComponent<QueryViewProps, QueryViewStat
/>
{this.renderAutoRunSwitch()}
{this.renderWrapQueryLimitSelector()}
- {result && (
- <QueryExtraInfo
- queryExtraInfo={result.queryExtraInfo}
- onDownload={this.handleDownload}
- />
+ {queryResult && (
+ <QueryExtraInfo queryResult={queryResult}
onDownload={this.handleDownload} />
)}
</div>
</div>
@@ -542,19 +494,30 @@ export class QueryView extends
React.PureComponent<QueryViewProps, QueryViewStat
runeMode={runeMode}
loading={loading}
error={error}
- queryResult={result ? result.queryResult : undefined}
- parsedQuery={result ? result.parsedQuery : undefined}
- onQueryChange={this.handleQueryStringChange}
+ queryResult={queryResult}
+ onQueryChange={this.handleQueryChange}
/>
</SplitterLayout>
);
}
- private handleQueryStringChange = (
- queryString: string | SqlQuery,
- preferablyRun?: boolean,
- ): void => {
- if (queryString instanceof SqlQuery) queryString = queryString.toString();
+ private handleQueryChange = (query: SqlQuery, preferablyRun?: boolean): void
=> {
+ this.handleQueryStringChange(query.toString(), preferablyRun);
+
+ // Possibly move the cursor of the QueryInput to the empty literal position
+ const emptyLiteralPosition = findEmptyLiteralPosition(query);
+ if (emptyLiteralPosition) {
+ // Introduce a delay to let the new text appear
+ setTimeout(() => {
+ const currentQueryInput = this.queryInputRef.current;
+ if (currentQueryInput) {
+ currentQueryInput.goToRowColumn(emptyLiteralPosition);
+ }
+ }, 10);
+ }
+ };
+
+ private handleQueryStringChange = (queryString: string, preferablyRun?:
boolean): void => {
this.setState({ queryString, parsedQuery: parser(queryString) }, () => {
const { autoRun } = this.state;
if (preferablyRun && autoRun) this.handleRun();
@@ -589,7 +552,7 @@ export class QueryView extends
React.PureComponent<QueryViewProps, QueryViewStat
localStorageSetJson(LocalStorageKeys.QUERY_CONTEXT, queryContext);
this.setState({ queryHistory: newQueryHistory });
- this.sqlQueryManager.runQuery({ queryString, queryContext, wrapQueryLimit
});
+ this.queryManager.runQuery({ queryString, queryContext, wrapQueryLimit });
};
private handleExplain = () => {
@@ -614,8 +577,8 @@ export class QueryView extends
React.PureComponent<QueryViewProps, QueryViewStat
let defaultSchema;
let defaultTable;
if (parsedQuery instanceof SqlQuery) {
- defaultSchema = parsedQuery.getSchema();
- defaultTable = parsedQuery.getTableName();
+ defaultSchema = parsedQuery.getFirstSchema();
+ defaultTable = parsedQuery.getFirstTableName();
}
return (
@@ -627,7 +590,7 @@ export class QueryView extends
React.PureComponent<QueryViewProps, QueryViewStat
getParsedQuery={this.getParsedQuery}
columnMetadataLoading={columnMetadataLoading}
columnMetadata={columnMetadata}
- onQueryStringChange={this.handleQueryStringChange}
+ onQueryChange={this.handleQueryChange}
defaultSchema={defaultSchema ? defaultSchema : 'druid'}
defaultTable={defaultTable}
/>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]