This is an automated email from the ASF dual-hosted git repository.
adarshsanjeev pushed a commit to branch 30.0.0
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/30.0.0 by this push:
new 7313903a94f Web console: Fix order-by-delta in explore view table
(#16417) (#16453)
7313903a94f is described below
commit 7313903a94f7530069f5c7e6f2507a8991b8ccd0
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Wed May 15 02:01:01 2024 -0700
Web console: Fix order-by-delta in explore view table (#16417) (#16453)
* change to using measure name
* Implment order by delta
* less paring, stricter types
* safeDivide0
* fix no query
* new DTQ alows parsing JSON_VALUE(...RETURNING...)
---
licenses.yaml | 2 +-
web-console/package-lock.json | 14 +-
web-console/package.json | 2 +-
.../record-table-pane/record-table-pane.tsx | 2 +-
.../async-action-dialog/async-action-dialog.tsx | 4 +-
.../kill-datasource-dialog.tsx | 7 +-
.../src/druid-models/execution/execution.ts | 10 +-
.../workbench-query/workbench-query.ts | 5 +-
web-console/src/utils/general.tsx | 16 +
web-console/src/utils/table-helpers.ts | 12 +-
.../views/datasources-view/datasources-view.tsx | 31 +-
.../generic-output-table/generic-output-table.tsx | 56 +--
.../explore-view/modules/table-react-module.tsx | 411 +++++++++++++++------
.../views/explore-view/modules/utils/utils.spec.ts | 57 ++-
.../src/views/explore-view/modules/utils/utils.ts | 63 +++-
web-console/src/views/explore-view/utils/misc.ts | 2 +-
.../src/views/lookups-view/lookups-view.tsx | 14 +-
.../src/views/services-view/services-view.tsx | 14 +-
.../schema-step/preview-table/preview-table.tsx | 2 +-
.../schema-step/schema-step.tsx | 2 +-
.../flexible-query-input/flexible-query-input.tsx | 2 +-
.../result-table-pane/result-table-pane.tsx | 2 +-
22 files changed, 553 insertions(+), 177 deletions(-)
diff --git a/licenses.yaml b/licenses.yaml
index ff0c0aaaa1f..248e87990b5 100644
--- a/licenses.yaml
+++ b/licenses.yaml
@@ -5108,7 +5108,7 @@ license_category: binary
module: web-console
license_name: Apache License version 2.0
copyright: Imply Data
-version: 0.22.11
+version: 0.22.13
---
diff --git a/web-console/package-lock.json b/web-console/package-lock.json
index f019362c882..db4b29d1569 100644
--- a/web-console/package-lock.json
+++ b/web-console/package-lock.json
@@ -14,7 +14,7 @@
"@blueprintjs/datetime2": "^0.9.35",
"@blueprintjs/icons": "^4.16.0",
"@blueprintjs/popover2": "^1.14.9",
- "@druid-toolkit/query": "^0.22.11",
+ "@druid-toolkit/query": "^0.22.13",
"@druid-toolkit/visuals-core": "^0.3.3",
"@druid-toolkit/visuals-react": "^0.3.3",
"ace-builds": "~1.4.14",
@@ -1004,9 +1004,9 @@
}
},
"node_modules/@druid-toolkit/query": {
- "version": "0.22.11",
- "resolved":
"https://registry.npmjs.org/@druid-toolkit/query/-/query-0.22.11.tgz",
- "integrity":
"sha512-VVEn/tsEr9fb+8eKc+nu3/YH7l+LZ1vd0D32UDo66GLS3cI+EKOCM7VYC8lTvB1tAS+98w/EzfbdlRPlkSeOoQ==",
+ "version": "0.22.13",
+ "resolved":
"https://registry.npmjs.org/@druid-toolkit/query/-/query-0.22.13.tgz",
+ "integrity":
"sha512-p0Cmmbk55vLaYs2WWcUr09qDRU2IrkXOxGgUG+wS6Uuq/ALBqSmUDlbMSxB3vJjMvegiwgJ8+n7VfVpO0t/bJg==",
"dependencies": {
"tslib": "^2.5.2"
}
@@ -19146,9 +19146,9 @@
"dev": true
},
"@druid-toolkit/query": {
- "version": "0.22.11",
- "resolved":
"https://registry.npmjs.org/@druid-toolkit/query/-/query-0.22.11.tgz",
- "integrity":
"sha512-VVEn/tsEr9fb+8eKc+nu3/YH7l+LZ1vd0D32UDo66GLS3cI+EKOCM7VYC8lTvB1tAS+98w/EzfbdlRPlkSeOoQ==",
+ "version": "0.22.13",
+ "resolved":
"https://registry.npmjs.org/@druid-toolkit/query/-/query-0.22.13.tgz",
+ "integrity":
"sha512-p0Cmmbk55vLaYs2WWcUr09qDRU2IrkXOxGgUG+wS6Uuq/ALBqSmUDlbMSxB3vJjMvegiwgJ8+n7VfVpO0t/bJg==",
"requires": {
"tslib": "^2.5.2"
}
diff --git a/web-console/package.json b/web-console/package.json
index a7c4c64e51d..9789123bf02 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -68,7 +68,7 @@
"@blueprintjs/datetime2": "^0.9.35",
"@blueprintjs/icons": "^4.16.0",
"@blueprintjs/popover2": "^1.14.9",
- "@druid-toolkit/query": "^0.22.11",
+ "@druid-toolkit/query": "^0.22.13",
"@druid-toolkit/visuals-core": "^0.3.3",
"@druid-toolkit/visuals-react": "^0.3.3",
"ace-builds": "~1.4.14",
diff --git a/web-console/src/components/record-table-pane/record-table-pane.tsx
b/web-console/src/components/record-table-pane/record-table-pane.tsx
index a2849ed0d5c..bfd9b644de9 100644
--- a/web-console/src/components/record-table-pane/record-table-pane.tsx
+++ b/web-console/src/components/record-table-pane/record-table-pane.tsx
@@ -104,7 +104,7 @@ export const RecordTablePane = React.memo(function
RecordTablePane(props: Record
const finalPage =
hasMoreResults && Math.floor(queryResult.rows.length /
pagination.pageSize) === pagination.page; // on the last page
- const numericColumnBraces = getNumericColumnBraces(queryResult, pagination);
+ const numericColumnBraces = getNumericColumnBraces(queryResult, undefined,
pagination);
return (
<div className={classNames('record-table-pane', { 'more-results':
hasMoreResults })}>
{finalPage ? (
diff --git
a/web-console/src/dialogs/async-action-dialog/async-action-dialog.tsx
b/web-console/src/dialogs/async-action-dialog/async-action-dialog.tsx
index e36ae511271..b8816fd493a 100644
--- a/web-console/src/dialogs/async-action-dialog/async-action-dialog.tsx
+++ b/web-console/src/dialogs/async-action-dialog/async-action-dialog.tsx
@@ -38,8 +38,8 @@ export interface AsyncActionDialogProps {
className?: string;
icon?: IconName;
intent?: Intent;
- successText: string;
- failText: string;
+ successText: ReactNode;
+ failText: ReactNode;
warningChecks?: ReactNode[];
children?: ReactNode;
}
diff --git
a/web-console/src/dialogs/kill-datasource-dialog/kill-datasource-dialog.tsx
b/web-console/src/dialogs/kill-datasource-dialog/kill-datasource-dialog.tsx
index dba85268d00..f5e2ca8add6 100644
--- a/web-console/src/dialogs/kill-datasource-dialog/kill-datasource-dialog.tsx
+++ b/web-console/src/dialogs/kill-datasource-dialog/kill-datasource-dialog.tsx
@@ -66,7 +66,12 @@ export const KillDatasourceDialog = function
KillDatasourceDialog(
return resp.data;
}}
confirmButtonText="Permanently delete unused segments"
- successText="Kill task was issued. Unused segments in datasource will be
deleted"
+ successText={
+ <>
+ Kill task was issued. Unused segments in datasource <Tag
minimal>{datasource}</Tag> will
+ be deleted
+ </>
+ }
failText="Failed submit kill task"
intent={Intent.DANGER}
onClose={onClose}
diff --git a/web-console/src/druid-models/execution/execution.ts
b/web-console/src/druid-models/execution/execution.ts
index 0cf8d5d0ed3..799de6f9c51 100644
--- a/web-console/src/druid-models/execution/execution.ts
+++ b/web-console/src/druid-models/execution/execution.ts
@@ -440,7 +440,10 @@ export class Execution {
value.queryContext = queryContext;
const parsedQuery = parseSqlQuery(sqlQuery);
if (value.result && (parsedQuery || queryContext)) {
- value.result = value.result.attachQuery({ context: queryContext },
parsedQuery);
+ value.result = value.result.attachQuery(
+ { ...this.nativeQuery, context: queryContext },
+ parsedQuery,
+ );
}
return new Execution(value);
@@ -463,7 +466,10 @@ export class Execution {
public changeResult(result: QueryResult): Execution {
return new Execution({
...this.valueOf(),
- result: result.attachQuery({}, this.sqlQuery ?
parseSqlQuery(this.sqlQuery) : undefined),
+ result: result.attachQuery(
+ this.nativeQuery,
+ this.sqlQuery ? parseSqlQuery(this.sqlQuery) : undefined,
+ ),
});
}
diff --git a/web-console/src/druid-models/workbench-query/workbench-query.ts
b/web-console/src/druid-models/workbench-query/workbench-query.ts
index d59cdbbe92e..e912e61ed57 100644
--- a/web-console/src/druid-models/workbench-query/workbench-query.ts
+++ b/web-console/src/druid-models/workbench-query/workbench-query.ts
@@ -18,6 +18,7 @@
import type {
QueryParameter,
+ QueryPayload,
SqlClusteredByClause,
SqlExpression,
SqlPartitionedByClause,
@@ -446,7 +447,7 @@ export class WorkbenchQuery {
public getApiQuery(makeQueryId: () => string = uuidv4): {
engine: DruidEngine;
- query: Record<string, any>;
+ query: QueryPayload;
prefixLines: number;
cancelQueryId?: string;
} {
@@ -478,7 +479,7 @@ export class WorkbenchQuery {
};
}
- let apiQuery: Record<string, any> = {};
+ let apiQuery: QueryPayload;
if (this.isJsonLike()) {
try {
apiQuery = Hjson.parse(queryString);
diff --git a/web-console/src/utils/general.tsx
b/web-console/src/utils/general.tsx
index b4537a63e08..1ea872b68f2 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -338,6 +338,22 @@ export function pluralIfNeeded(n: NumberLike, singular:
string, plural?: string)
// ----------------------------
+export function partition<T>(xs: T[], predicate: (x: T, i: number) =>
boolean): [T[], T[]] {
+ const match: T[] = [];
+ const nonMatch: T[] = [];
+
+ for (let i = 0; i < xs.length; i++) {
+ const x = xs[i];
+ if (predicate(x, i)) {
+ match.push(x);
+ } else {
+ nonMatch.push(x);
+ }
+ }
+
+ return [match, nonMatch];
+}
+
export function filterMap<T, Q>(xs: readonly T[], f: (x: T, i: number) => Q |
undefined): Q[] {
return xs.map(f).filter((x: Q | undefined) => typeof x !== 'undefined') as
Q[];
}
diff --git a/web-console/src/utils/table-helpers.ts
b/web-console/src/utils/table-helpers.ts
index 7eedd1acaab..a04635c61c5 100644
--- a/web-console/src/utils/table-helpers.ts
+++ b/web-console/src/utils/table-helpers.ts
@@ -32,9 +32,16 @@ export function changePage(pagination: Pagination, page:
number): Pagination {
return deepSet(pagination, 'page', page);
}
+export interface ColumnHint {
+ displayName?: string;
+ group?: string;
+ formatter?: (x: any) => string;
+}
+
export function getNumericColumnBraces(
queryResult: QueryResult,
- pagination?: Pagination,
+ columnHints: Map<string, ColumnHint> | undefined,
+ pagination: Pagination | undefined,
): Record<number, string[]> {
let rows = queryResult.rows;
@@ -47,8 +54,9 @@ export function getNumericColumnBraces(
if (rows.length) {
queryResult.header.forEach((column, i) => {
if (!oneOf(column.nativeType, 'LONG', 'FLOAT', 'DOUBLE')) return;
+ const formatter = columnHints?.get(column.name)?.formatter ||
formatNumber;
const brace = filterMap(rows, row =>
- oneOf(typeof row[i], 'number', 'bigint') ? formatNumber(row[i]) :
undefined,
+ oneOf(typeof row[i], 'number', 'bigint') ? formatter(row[i]) :
undefined,
);
if (rows.length === brace.length) {
numericColumnBraces[i] = brace;
diff --git a/web-console/src/views/datasources-view/datasources-view.tsx
b/web-console/src/views/datasources-view/datasources-view.tsx
index 713df9b18b1..54b11a5a0cb 100644
--- a/web-console/src/views/datasources-view/datasources-view.tsx
+++ b/web-console/src/views/datasources-view/datasources-view.tsx
@@ -16,7 +16,7 @@
* limitations under the License.
*/
-import { FormGroup, InputGroup, Intent, MenuItem, Switch } from
'@blueprintjs/core';
+import { FormGroup, InputGroup, Intent, MenuItem, Switch, Tag } from
'@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { SqlQuery, T } from '@druid-toolkit/query';
import classNames from 'classnames';
@@ -651,8 +651,18 @@ GROUP BY 1, 2`;
return resp.data;
}}
confirmButtonText="Mark as unused all segments"
- successText="All segments in datasource have been marked as unused"
- failText="Failed to mark as unused all segments in datasource"
+ successText={
+ <>
+ All segments in datasource <Tag
minimal>{datasourceToMarkAsUnusedAllSegmentsIn}</Tag>{' '}
+ have been marked as unused
+ </>
+ }
+ failText={
+ <>
+ Failed to mark as unused all segments in datasource{' '}
+ <Tag minimal>{datasourceToMarkAsUnusedAllSegmentsIn}</Tag>
+ </>
+ }
intent={Intent.DANGER}
onClose={() => {
this.setState({ datasourceToMarkAsUnusedAllSegmentsIn: undefined });
@@ -684,8 +694,19 @@ GROUP BY 1, 2`;
return resp.data;
}}
confirmButtonText="Mark as used all segments"
- successText="All non-overshadowed segments in datasource have been
marked as used"
- failText="Failed to mark as used all non-overshadowed segments in
datasource"
+ successText={
+ <>
+ All non-overshadowed segments in datasource{' '}
+ <Tag
minimal>{datasourceToMarkAllNonOvershadowedSegmentsAsUsedIn}</Tag> have been
marked
+ as used
+ </>
+ }
+ failText={
+ <>
+ Failed to mark as used all non-overshadowed segments in
datasource{' '}
+ <Tag
minimal>{datasourceToMarkAllNonOvershadowedSegmentsAsUsedIn}</Tag>
+ </>
+ }
intent={Intent.PRIMARY}
onClose={() => {
this.setState({ datasourceToMarkAllNonOvershadowedSegmentsAsUsedIn:
undefined });
diff --git
a/web-console/src/views/explore-view/modules/components/generic-output-table/generic-output-table.tsx
b/web-console/src/views/explore-view/modules/components/generic-output-table/generic-output-table.tsx
index 255a3a9b6fa..4f79156175b 100644
---
a/web-console/src/views/explore-view/modules/components/generic-output-table/generic-output-table.tsx
+++
b/web-console/src/views/explore-view/modules/components/generic-output-table/generic-output-table.tsx
@@ -31,7 +31,7 @@ import ReactTable from 'react-table';
import { BracedText, Deferred, TableCell } from '../../../../../components';
import { possibleDruidFormatForValues, TIME_COLUMN } from
'../../../../../druid-models';
import { SMALL_TABLE_PAGE_SIZE, SMALL_TABLE_PAGE_SIZE_OPTIONS } from
'../../../../../react-table';
-import type { Pagination, QueryAction } from '../../../../../utils';
+import type { ColumnHint, Pagination, QueryAction } from
'../../../../../utils';
import {
columnToIcon,
columnToWidth,
@@ -60,30 +60,34 @@ function isComparable(x: unknown): boolean {
return x !== null && x !== '';
}
-function columnNester(columns: TableColumn[], groupHints: string[] |
undefined): TableColumn[] {
- if (!groupHints) return columns;
+function columnNester(
+ tableColumns: TableColumn[],
+ resultColumns: readonly Column[],
+ columnHints: Map<string, ColumnHint> | undefined,
+): TableColumn[] {
+ if (!columnHints) return tableColumns;
const ret: TableColumn[] = [];
- let currentGroupHint: string | null = null;
+ let currentGroupName: string | null = null;
let currentColumnGroup: TableColumn | null = null;
- for (let i = 0; i < columns.length; i++) {
- const column = columns[i];
- const groupHint = groupHints[i];
- if (groupHint) {
- if (currentGroupHint === groupHint) {
- currentColumnGroup!.columns!.push(column);
+ for (let i = 0; i < tableColumns.length; i++) {
+ const tableColumn = tableColumns[i];
+ const group = columnHints.get(resultColumns[i].name)?.group;
+ if (group) {
+ if (currentGroupName === group) {
+ currentColumnGroup!.columns!.push(tableColumn);
} else {
- currentGroupHint = groupHint;
+ currentGroupName = group;
ret.push(
(currentColumnGroup = {
- Header: <div className="group-cell">{currentGroupHint}</div>,
- columns: [column],
+ Header: <div className="group-cell">{currentGroupName}</div>,
+ columns: [tableColumn],
}),
);
}
} else {
- ret.push(column);
- currentGroupHint = null;
+ ret.push(tableColumn);
+ currentGroupName = null;
currentColumnGroup = null;
}
}
@@ -94,12 +98,12 @@ function columnNester(columns: TableColumn[], groupHints:
string[] | undefined):
export interface GenericOutputTableProps {
queryResult: QueryResult;
onQueryAction(action: QueryAction): void;
- onOrderByChange?(columnIndex: number, desc: boolean): void;
+ onOrderByChange?(columnName: string, desc: boolean): void;
onExport?(): void;
runeMode: boolean;
showTypeIcons: boolean;
initPageSize?: number;
- groupHints?: string[];
+ columnHints?: Map<string, ColumnHint>;
}
export const GenericOutputTable = React.memo(function GenericOutputTable(
@@ -113,7 +117,7 @@ export const GenericOutputTable = React.memo(function
GenericOutputTable(
runeMode,
showTypeIcons,
initPageSize,
- groupHints,
+ columnHints,
} = props;
const parsedQuery = queryResult.sqlQuery;
const [pagination, setPagination] = useState<Pagination>({
@@ -159,7 +163,7 @@ export const GenericOutputTable = React.memo(function
GenericOutputTable(
icon={reverseOrderByDirection === 'ASC' ? IconNames.SORT_ASC :
IconNames.SORT_DESC}
text={`Order ${reverseOrderByDirection === 'ASC' ? 'ascending' :
'descending'}`}
onClick={() => {
- onOrderByChange(headerIndex, reverseOrderByDirection !==
'ASC');
+ onOrderByChange(header, reverseOrderByDirection !== 'ASC');
}}
/>,
);
@@ -170,7 +174,7 @@ export const GenericOutputTable = React.memo(function
GenericOutputTable(
icon={IconNames.SORT_DESC}
text="Order descending"
onClick={() => {
- onOrderByChange(headerIndex, true);
+ onOrderByChange(header, true);
}}
/>,
<MenuItem
@@ -178,7 +182,7 @@ export const GenericOutputTable = React.memo(function
GenericOutputTable(
icon={IconNames.SORT_ASC}
text="Order ascending"
onClick={() => {
- onOrderByChange(headerIndex, false);
+ onOrderByChange(header, false);
}}
/>,
);
@@ -426,7 +430,7 @@ export const GenericOutputTable = React.memo(function
GenericOutputTable(
const finalPage =
hasMoreResults && Math.floor(queryResult.rows.length /
pagination.pageSize) === pagination.page; // on the last page
- const numericColumnBraces = getNumericColumnBraces(queryResult, pagination);
+ const numericColumnBraces = getNumericColumnBraces(queryResult, columnHints,
pagination);
return (
<div className={classNames('generic-output-table', { 'more-results':
hasMoreResults })}>
{finalPage ? (
@@ -479,7 +483,7 @@ export const GenericOutputTable = React.memo(function
GenericOutputTable(
<div className="clickable-cell">
<div className="output-name">
{icon && <Icon className="type-icon" icon={icon}
size={12} />}
- {h}
+ {columnHints?.get(h)?.displayName ?? h}
{hasFilterOnHeader(h, i) && <Icon
icon={IconNames.FILTER} size={14} />}
</div>
</div>
@@ -490,6 +494,7 @@ export const GenericOutputTable = React.memo(function
GenericOutputTable(
accessor: String(i),
Cell(row) {
const value = row.value;
+ const formatter = columnHints?.get(h)?.formatter ||
formatNumber;
return (
<div>
<Popover2
@@ -498,7 +503,7 @@ export const GenericOutputTable = React.memo(function
GenericOutputTable(
{numericColumnBraces[i] ? (
<BracedText
className="table-padding"
- text={formatNumber(value)}
+ text={formatter(value)}
braces={numericColumnBraces[i]}
padFractionalPart
/>
@@ -516,7 +521,8 @@ export const GenericOutputTable = React.memo(function
GenericOutputTable(
: undefined,
};
}),
- groupHints,
+ queryResult.header,
+ columnHints,
)}
/>
)}
diff --git a/web-console/src/views/explore-view/modules/table-react-module.tsx
b/web-console/src/views/explore-view/modules/table-react-module.tsx
index 2f20a44b112..fcc06a09784 100644
--- a/web-console/src/views/explore-view/modules/table-react-module.tsx
+++ b/web-console/src/views/explore-view/modules/table-react-module.tsx
@@ -16,11 +16,13 @@
* limitations under the License.
*/
-import type { SqlOrderByExpression } from '@druid-toolkit/query';
+import { Button } from '@blueprintjs/core';
+import type { SqlOrderByExpression, SqlTable } from '@druid-toolkit/query';
import {
C,
F,
SqlCase,
+ SqlColumn,
SqlExpression,
SqlFunction,
SqlLiteral,
@@ -35,15 +37,23 @@ import ReactDOM from 'react-dom';
import { Loader } from '../../../components';
import { useQueryManager } from '../../../hooks';
+import type { ColumnHint } from '../../../utils';
+import { formatInteger, formatPercent } from '../../../utils';
import { getInitQuery } from '../utils';
import { GenericOutputTable } from './components';
-import { shiftTimeInWhere } from './utils/utils';
+import { getWhereForCompares, shiftTimeInExpression } from './utils/utils';
import './table-react-module.scss';
type MultipleValueMode = 'null' | 'empty' | 'latest' | 'latestNonNull' |
'count';
+type CompareType = 'value' | 'delta' | 'absDelta' | 'percent' | 'absPercent';
+
+// As of this writing ordering the outer query on something other than __time
sometimes throws an error, set this to false / remove it
+// when ordering on non __time is more robust
+const NEEDS_GROUPING_TO_ORDER = true;
+
const KNOWN_AGGREGATIONS = [
'COUNT',
'SUM',
@@ -73,13 +83,37 @@ const KNOWN_AGGREGATIONS = [
'ANY_VALUE',
];
+const TOP_VALUES_NAME = 'top_values';
+const TOP_VALUES_K = 5000;
+
+function coalesce0(ex: SqlExpression) {
+ return F('COALESCE', ex, SqlLiteral.ZERO);
+}
+
+function safeDivide0(a: SqlExpression, b: SqlExpression) {
+ return coalesce0(F('SAFE_DIVIDE', a, b));
+}
+
+function anyValue(ex: SqlExpression) {
+ return F('ANY_VALUE', ex);
+}
+
+function addTableScope(expression: SqlExpression, newTableScope: string):
SqlExpression {
+ return expression.walk(ex => {
+ if (ex instanceof SqlColumn && !ex.getTableName()) {
+ return ex.changeTableName(newTableScope);
+ }
+ return ex;
+ }) as SqlExpression;
+}
+
function toGroupByExpression(
splitColumn: ExpressionMeta,
timeBucket: string,
compareShiftDuration?: string,
) {
const { expression, sqlType, name } = splitColumn;
- return expression
+ return addTableScope(expression, 't')
.applyIf(sqlType === 'TIMESTAMP' && compareShiftDuration, e =>
F.timeShift(e, compareShiftDuration!, 1),
)
@@ -131,9 +165,21 @@ function toShowColumnExpression(
return ex.as(showColumn.name);
}
+function getJoinCondition(
+ splitColumns: ExpressionMeta[],
+ table1: SqlTable,
+ table2: SqlTable,
+): SqlExpression {
+ return SqlExpression.and(
+ ...splitColumns.map(splitColumn =>
+
table1.column(splitColumn.name).isNotDistinctFrom(table2.column(splitColumn.name)),
+ ),
+ );
+}
+
interface QueryAndHints {
query: SqlQuery;
- groupHints: string[];
+ columnHints: Map<string, ColumnHint>;
}
export default typedVisualModule({
@@ -200,13 +246,14 @@ export default typedVisualModule({
compares: {
type: 'options',
- options: ['PT1M', 'PT5M', 'PT1H', 'P1D', 'P1M'],
+ options: ['PT1M', 'PT5M', 'PT1H', 'PT6H', 'P1D', 'P1M'],
control: {
label: 'Compares',
optionLabels: {
PT1M: '1 minute',
PT5M: '5 minutes',
PT1H: '1 hour',
+ PT6H: '6 hours',
P1D: '1 day',
P1M: '1 month',
},
@@ -214,12 +261,31 @@ export default typedVisualModule({
},
},
- showDelta: {
+ compareTypes: {
+ type: 'options',
+ options: ['value', 'delta', 'absDelta', 'percent', 'absPercent'],
+ default: ['value', 'delta'],
+ control: {
+ label: 'Compare types',
+ visible: ({ params }) => Boolean((params.compares || []).length) &&
!params.pivotColumn,
+ optionLabels: {
+ value: 'Value',
+ delta: 'Delta',
+ absDelta: 'Abs. delta',
+ percent: 'Percent',
+ absPercent: 'Abs. percent',
+ },
+ },
+ },
+ restrictTop: {
type: 'boolean',
+ default: true,
control: {
- visible: ({ params }) => Boolean((params.compares || []).length),
+ label: `Restrict to top ${formatInteger(TOP_VALUES_K)} when ordering
on delta`,
+ visible: ({ params }) => Boolean((params.compares || []).length) &&
!params.pivotColumn,
},
},
+
maxRows: {
type: 'number',
default: 200,
@@ -287,7 +353,7 @@ function TableModule(props: TableModuleProps) {
},
});
- const queryAndHints = useMemo(() => {
+ const queryAndHints = useMemo((): QueryAndHints | undefined => {
const splitColumns: ExpressionMeta[] = parameterValues.splitColumns;
const timeBucket: string = parameterValues.timeBucket || 'PT1H';
const showColumns: ExpressionMeta[] = parameterValues.showColumns;
@@ -295,26 +361,71 @@ function TableModule(props: TableModuleProps) {
const pivotColumn: ExpressionMeta = parameterValues.pivotColumn;
const metrics: ExpressionMeta[] = parameterValues.metrics;
const compares: string[] = parameterValues.compares || [];
- const showDelta: boolean = parameterValues.showDelta;
+ const compareTypes: CompareType[] = parameterValues.compareTypes;
+ const restrictTop: boolean = parameterValues.restrictTop;
const maxRows: number = parameterValues.maxRows;
const pivotValues = pivotColumn ? pivotValueState.data : undefined;
if (pivotColumn && !pivotValues) return;
- const hasCompare = Boolean(compares.length);
+ const effectiveOrderBy =
+ orderBy || C(metrics[0]?.name ||
splitColumns[0]?.name).toOrderByExpression('DESC');
+ const hasCompare = !pivotColumn && Boolean(compares.length) &&
Boolean(compareTypes.length);
+
+ const orderByColumnName = (effectiveOrderBy.expression as
SqlColumn).getName();
+ let orderByCompareMeasure: string | undefined;
+ let orderByCompareDuration: string | undefined;
+ let orderByCompareType: CompareType | undefined;
+ if (hasCompare) {
+ const m = orderByColumnName.match(
+ /^(.+):cmp:([^:]+):(value|delta|absDelta|percent|absPercent)$/,
+ );
+ if (m) {
+ orderByCompareMeasure = m[1];
+ orderByCompareDuration = m[2];
+ orderByCompareType = m[3] as CompareType;
+ }
+ }
+
+ const metricExpression = metrics.find(m => m.name ===
orderByCompareMeasure)?.expression;
+ const topValuesQuery =
+ restrictTop && metricExpression && orderByCompareType !== 'value' &&
splitColumns.length
+ ? getInitQuery(table, getWhereForCompares(where, compares))
+ .applyForEach(splitColumns, (q, splitColumn) =>
+ q.addSelect(toGroupByExpression(splitColumn, timeBucket), {
+ addToGroupBy: 'end',
+ }),
+ )
+
.changeOrderByExpression(metricExpression.toOrderByExpression('DESC'))
+ .changeLimitValue(TOP_VALUES_K)
+ : undefined;
+
+ const columnHints = new Map<string, ColumnHint>();
const mainQuery = getInitQuery(table, where)
+ .applyIf(topValuesQuery, q =>
+ q.addInnerJoin(
+ T(TOP_VALUES_NAME),
+ getJoinCondition(splitColumns, T('t'), T(TOP_VALUES_NAME)),
+ ),
+ )
.applyForEach(splitColumns, (q, splitColumn) =>
q.addSelect(toGroupByExpression(splitColumn, timeBucket), {
addToGroupBy: 'end',
}),
)
- .applyForEach(showColumns, (q, showColumn) =>
- q.addSelect(toShowColumnExpression(showColumn, multipleValueMode)),
+ .applyIf(!orderByCompareDuration, q =>
+ q.applyForEach(showColumns, (q, showColumn) =>
+ q.addSelect(toShowColumnExpression(showColumn, multipleValueMode)),
+ ),
)
.applyForEach(pivotValues || [''], (q, pivotValue, i) =>
- q.applyForEach(metrics, (q, metric) =>
- q.addSelect(
+ q.applyForEach(metrics, (q, metric) => {
+ const alias = `${metric.name}${pivotColumn && i > 0 ?
`:${pivotValue}` : ''}`;
+ if (pivotColumn) {
+ columnHints.set(alias, { displayName: metric.name, group:
pivotValue });
+ }
+ return q.addSelect(
metric.expression
.as(metric.name)
.applyIf(pivotColumn, q =>
@@ -323,115 +434,204 @@ function TableModule(props: TableModuleProps) {
pivotColumn.expression.equal(pivotValue),
KNOWN_AGGREGATIONS,
)
- .as(`${metric.name}${i > 0 ? ` [${pivotValue}]` : ''}`),
+ .as(alias),
),
- ),
- ),
- )
- .applyIf(metrics.length > 0 || splitColumns.length > 0, q =>
- q.changeOrderByExpression(
- orderBy || C(metrics[0]?.name ||
splitColumns[0]?.name).toOrderByExpression('DESC'),
- ),
+ );
+ }),
)
- .changeLimitValue(maxRows);
+ .applyIf(!orderByCompareDuration, q =>
+ q
+ .applyIf(metrics.length > 0 || splitColumns.length > 0, q =>
+ q.changeOrderByExpression(effectiveOrderBy),
+ )
+ .changeLimitValue(maxRows),
+ );
if (!hasCompare) {
return {
query: mainQuery,
- groupHints: pivotColumn
- ? splitColumns
- .map(() => '')
- .concat(
- showColumns.map(() => ''),
- (pivotValues || []).flatMap(v => metrics.map(() => v)),
- )
- : [],
+ columnHints,
};
}
const main = T('main');
- return {
- query: SqlQuery.from(main)
- .changeWithParts(
- [SqlWithPart.simple('main', mainQuery)].concat(
- compares.map((comparePeriod, i) =>
- SqlWithPart.simple(
- `compare${i}`,
- getInitQuery(table, shiftTimeInWhere(where, comparePeriod))
- .applyForEach(splitColumns, (q, splitColumn) =>
- q.addSelect(toGroupByExpression(splitColumn, timeBucket,
comparePeriod), {
- addToGroupBy: 'end',
- }),
- )
- .applyForEach(metrics, (q, metric) =>
- q.addSelect(metric.expression.as(metric.name)),
+ const leader = T(orderByCompareDuration ?
`compare_${orderByCompareDuration}` : 'main');
+ const query = SqlQuery.from(leader)
+ .changeWithParts(
+ (
+ (topValuesQuery
+ ? [SqlWithPart.simple(TOP_VALUES_NAME, topValuesQuery)]
+ : []) as SqlWithPart[]
+ ).concat(
+ SqlWithPart.simple('main', mainQuery),
+ compares.map(compare =>
+ SqlWithPart.simple(
+ `compare_${compare}`,
+ getInitQuery(table, shiftTimeInExpression(where, compare))
+ .applyIf(topValuesQuery, q =>
+ q.addInnerJoin(
+ T(TOP_VALUES_NAME),
+ getJoinCondition(splitColumns, T('t'), T(TOP_VALUES_NAME)),
),
- ),
+ )
+ .applyForEach(splitColumns, (q, splitColumn) =>
+ q.addSelect(toGroupByExpression(splitColumn, timeBucket,
compare), {
+ addToGroupBy: 'end',
+ }),
+ )
+ .applyIf(orderByCompareDuration === compare, q =>
+ q.applyForEach(showColumns, (q, showColumn) =>
+ q.addSelect(toShowColumnExpression(showColumn,
multipleValueMode)),
+ ),
+ )
+ .applyForEach(metrics, (q, metric) =>
+ q.addSelect(metric.expression.as(metric.name)),
+ )
+ .applyIf(compare === orderByCompareDuration &&
orderByCompareType === 'value', q =>
+ q
+ .changeOrderByExpression(
+
effectiveOrderBy.changeExpression(C(orderByCompareMeasure!)),
+ )
+ .changeLimitValue(maxRows),
+ ),
),
),
- )
- .changeSelectExpressions(
- splitColumns
- .map(splitColumn =>
main.column(splitColumn.name).as(splitColumn.name))
- .concat(
- showColumns.map(showColumn =>
main.column(showColumn.name).as(showColumn.name)),
- metrics.map(metric => main.column(metric.name).as(metric.name)),
- compares.flatMap((_, i) =>
- metrics.flatMap(metric => {
- const c = T(`compare${i}`).column(metric.name);
-
- const ret = [SqlFunction.simple('COALESCE', [c,
0]).as(`#prev: ${metric.name}`)];
-
- if (showDelta) {
- ret.push(
- F.stringFormat(
- '%.1f%%',
- SqlFunction.simple('SAFE_DIVIDE', [
- SqlExpression.parse(`(${main.column(metric.name)} -
${c}) * 100.0`),
- c,
- ]),
- ).as(`%chg: ${metric.name}`),
- );
- }
-
- return ret;
- }),
- ),
+ ),
+ )
+ .changeSelectExpressions(
+ splitColumns
+ .map(splitColumn =>
main.column(splitColumn.name).as(splitColumn.name))
+ .concat(
+ showColumns.map(showColumn =>
+ leader
+ .column(showColumn.name)
+ .applyIf(NEEDS_GROUPING_TO_ORDER, anyValue)
+ .as(showColumn.name),
),
- )
- .applyForEach(compares, (q, _comparePeriod, i) =>
- q.addLeftJoin(
- T(`compare${i}`),
- SqlExpression.and(
- ...splitColumns.map(splitColumn =>
- main
- .column(splitColumn.name)
-
.isNotDistinctFrom(T(`compare${i}`).column(splitColumn.name)),
- ),
+ metrics.map(metric =>
+ main
+ .column(metric.name)
+ .applyIf(NEEDS_GROUPING_TO_ORDER, anyValue)
+ .applyIf(orderByCompareDuration, coalesce0)
+ .as(metric.name),
+ ),
+ compares.flatMap(compare =>
+ metrics.flatMap(metric => {
+ const c = T(`compare_${compare}`)
+ .column(metric.name)
+ .applyIf(NEEDS_GROUPING_TO_ORDER, anyValue)
+ .applyIf(compare !== orderByCompareDuration, coalesce0);
+
+ const mainMetric = main
+ .column(metric.name)
+ .applyIf(NEEDS_GROUPING_TO_ORDER, anyValue)
+ .applyIf(orderByCompareDuration, coalesce0);
+
+ const diff = mainMetric.subtract(c);
+
+ const ret: SqlExpression[] = [];
+
+ if (compareTypes.includes('value')) {
+ const valueName = `${metric.name}:cmp:${compare}:value`;
+ columnHints.set(valueName, {
+ group: `Comparison to ${compare}`,
+ displayName: `${metric.name} (value)`,
+ });
+ ret.push(c.as(valueName));
+ }
+
+ if (compareTypes.includes('delta')) {
+ const deltaName = `${metric.name}:cmp:${compare}:delta`;
+ columnHints.set(deltaName, {
+ group: `Comparison to ${compare}`,
+ displayName: `${metric.name} (delta)`,
+ });
+ ret.push(diff.as(deltaName));
+ }
+
+ if (compareTypes.includes('absDelta')) {
+ const deltaName = `${metric.name}:cmp:${compare}:absDelta`;
+ columnHints.set(deltaName, {
+ group: `Comparison to ${compare}`,
+ displayName: `${metric.name} (Abs. delta)`,
+ });
+ ret.push(F('ABS', diff).as(deltaName));
+ }
+
+ if (compareTypes.includes('percent')) {
+ const percentName = `${metric.name}:cmp:${compare}:percent`;
+ columnHints.set(percentName, {
+ group: `Comparison to ${compare}`,
+ displayName: `${metric.name} (%)`,
+ formatter: formatPercent,
+ });
+ ret.push(
+ safeDivide0(diff.multiply(SqlLiteral.ONE_POINT_ZERO),
c).as(percentName),
+ );
+ }
+
+ if (compareTypes.includes('absPercent')) {
+ const percentName =
`${metric.name}:cmp:${compare}:absPercent`;
+ columnHints.set(percentName, {
+ group: `Comparison to ${compare}`,
+ displayName: `${metric.name} (abs. %)`,
+ formatter: formatPercent,
+ });
+ ret.push(
+ F('ABS',
safeDivide0(diff.multiply(SqlLiteral.ONE_POINT_ZERO), c)).as(
+ percentName,
+ ),
+ );
+ }
+
+ return ret;
+ }),
),
),
+ )
+ .applyIf(orderByCompareDuration, q =>
+ q.addLeftJoin(
+ main,
+ getJoinCondition(splitColumns, main,
T(`compare_${orderByCompareDuration}`)),
),
- groupHints: splitColumns
- .map(() => 'Current')
- .concat(
- showColumns.map(() => 'Current'),
- metrics.map(() => 'Current'),
- compares.flatMap(comparePeriod =>
- metrics
- .flatMap(() => (showDelta ? ['', ''] : ['']))
- .map(() => `Comparison to ${comparePeriod}`),
+ )
+ .applyForEach(
+ compares.filter(c => c !== orderByCompareDuration),
+ (q, compare) =>
+ q.addLeftJoin(
+ T(`compare_${compare}`),
+ getJoinCondition(splitColumns, main, T(`compare_${compare}`)),
),
- ),
+ )
+ .applyIf(NEEDS_GROUPING_TO_ORDER, q =>
+ q.changeGroupByExpressions(splitColumns.map((_, i) =>
SqlLiteral.index(i))),
+ )
+ .addOrderBy(effectiveOrderBy)
+ .changeLimitValue(maxRows);
+
+ for (const splitColumn of splitColumns) {
+ columnHints.set(splitColumn.name, { group: 'Current' });
+ }
+ for (const showColumn of showColumns) {
+ columnHints.set(showColumn.name, { group: 'Current' });
+ }
+ for (const metric of metrics) {
+ columnHints.set(metric.name, { group: 'Current' });
+ }
+
+ return {
+ query,
+ columnHints,
};
}, [table, where, parameterValues, orderBy, pivotValueState.data]);
const [resultState] = useQueryManager({
query: queryAndHints,
processQuery: async (queryAndHints: QueryAndHints) => {
- const { query, groupHints } = queryAndHints;
+ const { query, columnHints } = queryAndHints;
return {
result: await sqlQuery(query),
- groupHints,
+ columnHints,
};
},
});
@@ -440,19 +640,24 @@ function TableModule(props: TableModuleProps) {
return (
<div className="table-module">
{resultState.error ? (
- resultState.getErrorMessage()
+ <div>
+ <div>{resultState.getErrorMessage()}</div>
+ {resultState.getErrorMessage()?.includes('not found in any table')
&& orderBy && (
+ <Button text="Clear order by" onClick={() =>
setOrderBy(undefined)} />
+ )}
+ </div>
) : resultData ? (
<GenericOutputTable
runeMode={false}
queryResult={resultData.result}
- groupHints={resultData.groupHints}
+ columnHints={resultData.columnHints}
showTypeIcons={false}
- onOrderByChange={(headerIndex, desc) => {
- const idx = SqlLiteral.index(headerIndex);
- if (orderBy && String(orderBy.expression) === String(idx)) {
+ onOrderByChange={(columnName, desc) => {
+ const column = C(columnName);
+ if (orderBy && orderBy.expression.equals(column)) {
setOrderBy(orderBy.reverseDirection());
} else {
- setOrderBy(idx.toOrderByExpression(desc ? 'DESC' : 'ASC'));
+ setOrderBy(column.toOrderByExpression(desc ? 'DESC' : 'ASC'));
}
}}
onQueryAction={action => {
diff --git a/web-console/src/views/explore-view/modules/utils/utils.spec.ts
b/web-console/src/views/explore-view/modules/utils/utils.spec.ts
index 9db276e6a0b..022d0f4a829 100644
--- a/web-console/src/views/explore-view/modules/utils/utils.spec.ts
+++ b/web-console/src/views/explore-view/modules/utils/utils.spec.ts
@@ -18,21 +18,64 @@
import { SqlExpression } from '@druid-toolkit/query';
-import { shiftTimeInWhere } from './utils';
+import { getWhereForCompares, shiftTimeInExpression } from './utils';
-describe('shiftTimeInWhere', () => {
- it('works with TIME_IN_INTERVAL', () => {
+describe('getWhereForCompares', () => {
+ it('works', () => {
expect(
- shiftTimeInWhere(
+ getWhereForCompares(
+ SqlExpression.parse(
+ `TIME_IN_INTERVAL("__time", '2016-06-27/2016-06-28') AND "country" =
'United States'`,
+ ),
+ ['PT1H', 'P1D'],
+ ).toString(),
+ ).toEqual(
+ `(TIME_IN_INTERVAL("__time", '2016-06-27/2016-06-28') OR
(TIME_SHIFT(TIMESTAMP '2016-06-27', 'PT1H', -1) <= "__time" AND "__time" <
TIME_SHIFT(TIMESTAMP '2016-06-28', 'PT1H', -1)) OR (TIME_SHIFT(TIMESTAMP
'2016-06-27', 'P1D', -1) <= "__time" AND "__time" < TIME_SHIFT(TIMESTAMP
'2016-06-28', 'P1D', -1))) AND "country" = 'United States'`,
+ );
+ });
+});
+
+describe('shiftTimeInExpression', () => {
+ it('works with TIME_IN_INTERVAL (date)', () => {
+ expect(
+ shiftTimeInExpression(
SqlExpression.parse(`TIME_IN_INTERVAL("__time",
'2016-06-27/2016-06-28')`),
'P1D',
).toString(),
- ).toEqual(`TIME_IN_INTERVAL(TIME_SHIFT("__time", 'P1D', 1),
'2016-06-27/2016-06-28')`);
+ ).toEqual(
+ `TIME_SHIFT(TIMESTAMP '2016-06-27', 'P1D', -1) <= "__time" AND "__time"
< TIME_SHIFT(TIMESTAMP '2016-06-28', 'P1D', -1)`,
+ );
+ });
+
+ it('works with TIME_IN_INTERVAL (date and time)', () => {
+ expect(
+ shiftTimeInExpression(
+ SqlExpression.parse(
+ `TIME_IN_INTERVAL("__time",
'2016-06-27T12:34:56/2016-06-28T12:34:56')`,
+ ),
+ 'P1D',
+ ).toString(),
+ ).toEqual(
+ `TIME_SHIFT(TIMESTAMP '2016-06-27 12:34:56', 'P1D', -1) <= "__time" AND
"__time" < TIME_SHIFT(TIMESTAMP '2016-06-28 12:34:56', 'P1D', -1)`,
+ );
+ });
+
+ it('works with TIME_IN_INTERVAL (date and time, zulu)', () => {
+ expect(
+ shiftTimeInExpression(
+ SqlExpression.parse(
+ `TIME_IN_INTERVAL("__time",
'2016-06-27T12:34:56Z/2016-06-28T12:34:56Z')`,
+ ),
+ 'P1D',
+ ).toString(),
+ ).toEqual(
+ `TIME_SHIFT(TIME_PARSE('2016-06-27 12:34:56', NULL, 'Etc/UTC'), 'P1D',
-1) <= "__time" AND "__time" < TIME_SHIFT(TIME_PARSE('2016-06-28 12:34:56',
NULL, 'Etc/UTC'), 'P1D', -1)`,
+ );
});
it('works with relative time', () => {
expect(
- shiftTimeInWhere(
+ shiftTimeInExpression(
SqlExpression.parse(
`(TIME_SHIFT(MAX_DATA_TIME(), 'PT1H', -1) <= "__time" AND "__time" <
MAX_DATA_TIME())`,
),
@@ -45,7 +88,7 @@ describe('shiftTimeInWhere', () => {
it('works with relative time (specific timestamps)', () => {
expect(
- shiftTimeInWhere(
+ shiftTimeInExpression(
SqlExpression.parse(
`TIMESTAMP '2016-06-27 20:31:02.498' <= "__time" AND "__time" <
TIMESTAMP '2016-06-27 21:31:02.498'`,
),
diff --git a/web-console/src/views/explore-view/modules/utils/utils.ts
b/web-console/src/views/explore-view/modules/utils/utils.ts
index 7320ca52404..2783585d4fe 100644
--- a/web-console/src/views/explore-view/modules/utils/utils.ts
+++ b/web-console/src/views/explore-view/modules/utils/utils.ts
@@ -16,28 +16,77 @@
* limitations under the License.
*/
-import type { SqlExpression } from '@druid-toolkit/query';
-import { F, SqlFunction, SqlLiteral } from '@druid-toolkit/query';
+import { F, SqlExpression, SqlFunction, SqlLiteral } from
'@druid-toolkit/query';
-export function shiftTimeInWhere(where: SqlExpression, period: string):
SqlExpression {
- return where.walk(ex => {
+import { partition } from '../../../../utils';
+
+const IS_DATE_LIKE = /^[+-]?\d\d\d\d[^']+$/;
+
+function isoStringToTimestampLiteral(iso: string): SqlExpression {
+ const zulu = iso.endsWith('Z');
+ const cleanIso = iso.replace('T', ' ').replace('Z', '');
+ let sql: string;
+ if (zulu) {
+ sql = `TIME_PARSE('${cleanIso}', NULL, 'Etc/UTC')`;
+ } else {
+ sql = `TIMESTAMP '${cleanIso}'`;
+ }
+ return SqlExpression.parse(sql);
+}
+
+export function getWhereForCompares(where: SqlExpression, compares: string[]):
SqlExpression {
+ const whereParts = where.decomposeViaAnd({ flatten: true });
+ const [timeExpressions, timelessExpressions] = partition(whereParts,
expressionUsesTime);
+ return SqlExpression.and(
+ SqlExpression.or(
+ SqlExpression.and(...timeExpressions),
+ ...compares.map(compare =>
+ SqlExpression.and(
+ ...timeExpressions.map(timeExpression =>
shiftTimeInExpression(timeExpression, compare)),
+ ),
+ ),
+ ),
+ ...timelessExpressions,
+ );
+}
+
+function expressionUsesTime(expression: SqlExpression): boolean {
+ return shiftTimeInExpression(expression, 'P1D') !== expression;
+}
+
+export function shiftTimeInExpression(expression: SqlExpression, compare:
string): SqlExpression {
+ return expression.walk(ex => {
if (ex instanceof SqlLiteral) {
// Works with: __time < TIMESTAMP '2022-01-02 03:04:05'
if (ex.isDate()) {
- return F('TIME_SHIFT', ex, period, -1);
+ return F.timeShift(ex, compare, -1);
}
} else if (ex instanceof SqlFunction) {
const effectiveFunctionName = ex.getEffectiveFunctionName();
// Works with: TIME_IN_INTERVAL(__time, '<interval>')
if (effectiveFunctionName === 'TIME_IN_INTERVAL') {
- return ex.changeArgs(ex.args!.change(0, F('TIME_SHIFT', ex.getArg(0),
period, 1)));
+ // Ideally we could rewrite it to TIME_IN_INTERVAL(TIME_SHIFT(__time,
period, 1), '<interval>') but that would be slow in the current Druid
+ // return ex.changeArgs(ex.args!.change(0, F('TIME_SHIFT',
ex.getArg(0), period, 1)));a
+
+ const interval = ex.getArgAsString(1);
+ if (!interval) return ex;
+
+ const [start, end] = interval.split('/');
+ if (!IS_DATE_LIKE.test(start) || !IS_DATE_LIKE.test(end)) return ex;
+
+ const t = ex.getArg(0);
+ if (!t) return ex;
+
+ return F.timeShift(isoStringToTimestampLiteral(start), compare, -1)
+ .lessThanOrEqual(t)
+ .and(t.lessThan(F.timeShift(isoStringToTimestampLiteral(end),
compare, -1)));
}
// Works with: TIME_SHIFT(...) <= __time
// and: __time < MAX_DATA_TIME()
if (effectiveFunctionName === 'TIME_SHIFT' || effectiveFunctionName ===
'MAX_DATA_TIME') {
- return F('TIME_SHIFT', ex, period, -1);
+ return F.timeShift(ex, compare, -1);
}
}
diff --git a/web-console/src/views/explore-view/utils/misc.ts
b/web-console/src/views/explore-view/utils/misc.ts
index 69946dd99d5..05185264387 100644
--- a/web-console/src/views/explore-view/utils/misc.ts
+++ b/web-console/src/views/explore-view/utils/misc.ts
@@ -34,7 +34,7 @@ export function toggle<T>(xs: readonly T[], x: T, eq?: (a: T,
b: T) => boolean):
}
export function getInitQuery(table: SqlExpression, where: SqlExpression):
SqlQuery {
- return SqlQuery.from(table).applyIf(String(where) !== 'TRUE', q =>
+ return SqlQuery.from(table.as('t')).applyIf(String(where) !== 'TRUE', q =>
q.changeWhereExpression(where),
);
}
diff --git a/web-console/src/views/lookups-view/lookups-view.tsx
b/web-console/src/views/lookups-view/lookups-view.tsx
index af8207f6ab1..caeee7d3466 100644
--- a/web-console/src/views/lookups-view/lookups-view.tsx
+++ b/web-console/src/views/lookups-view/lookups-view.tsx
@@ -16,7 +16,7 @@
* limitations under the License.
*/
-import { Button, Icon, Intent } from '@blueprintjs/core';
+import { Button, Icon, Intent, Tag } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import React from 'react';
import type { Filter } from 'react-table';
@@ -295,8 +295,16 @@ export class LookupsView extends
React.PureComponent<LookupsViewProps, LookupsVi
);
}}
confirmButtonText="Delete lookup"
- successText="Lookup was deleted"
- failText="Could not delete lookup"
+ successText={
+ <>
+ Lookup <Tag minimal>{deleteLookupName}</Tag> was deleted
+ </>
+ }
+ failText={
+ <>
+ Could not delete lookup <Tag minimal>{deleteLookupName}</Tag>
+ </>
+ }
intent={Intent.DANGER}
onClose={() => {
this.setState({ deleteLookupTier: undefined, deleteLookupName:
undefined });
diff --git a/web-console/src/views/services-view/services-view.tsx
b/web-console/src/views/services-view/services-view.tsx
index 3ff6eead276..52de53f80de 100644
--- a/web-console/src/views/services-view/services-view.tsx
+++ b/web-console/src/views/services-view/services-view.tsx
@@ -16,7 +16,7 @@
* limitations under the License.
*/
-import { Button, ButtonGroup, Intent, Label, MenuItem } from
'@blueprintjs/core';
+import { Button, ButtonGroup, Intent, Label, MenuItem, Tag } from
'@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { sum } from 'd3-array';
import React from 'react';
@@ -699,8 +699,16 @@ ORDER BY
return resp.data;
}}
confirmButtonText="Disable worker"
- successText="Worker has been disabled"
- failText="Could not disable worker"
+ successText={
+ <>
+ Worker <Tag minimal>{middleManagerDisableWorkerHost}</Tag> has
been disabled
+ </>
+ }
+ failText={
+ <>
+ Could not disable worker <Tag
minimal>{middleManagerDisableWorkerHost}</Tag>
+ </>
+ }
intent={Intent.DANGER}
onClose={() => {
this.setState({ middleManagerDisableWorkerHost: undefined });
diff --git
a/web-console/src/views/sql-data-loader-view/schema-step/preview-table/preview-table.tsx
b/web-console/src/views/sql-data-loader-view/schema-step/preview-table/preview-table.tsx
index a674a3e6028..12e12716426 100644
---
a/web-console/src/views/sql-data-loader-view/schema-step/preview-table/preview-table.tsx
+++
b/web-console/src/views/sql-data-loader-view/schema-step/preview-table/preview-table.tsx
@@ -103,7 +103,7 @@ export const PreviewTable = React.memo(function
PreviewTable(props: PreviewTable
);
}
- const numericColumnBraces = getNumericColumnBraces(queryResult);
+ const numericColumnBraces = getNumericColumnBraces(queryResult, undefined,
undefined);
return (
<div className="preview-table">
<ReactTable
diff --git
a/web-console/src/views/sql-data-loader-view/schema-step/schema-step.tsx
b/web-console/src/views/sql-data-loader-view/schema-step/schema-step.tsx
index c52ee91abff..149843adc0a 100644
--- a/web-console/src/views/sql-data-loader-view/schema-step/schema-step.tsx
+++ b/web-console/src/views/sql-data-loader-view/schema-step/schema-step.tsx
@@ -515,7 +515,7 @@ export const SchemaStep = function SchemaStep(props:
SchemaStepProps) {
throw new DruidError(e);
}
- return result.attachQuery({}, SqlQuery.maybeParse(previewQueryString));
+ return result.attachQuery({} as any,
SqlQuery.maybeParse(previewQueryString));
}
},
backgroundStatusCheck: executionBackgroundResultStatusCheck,
diff --git
a/web-console/src/views/workbench-view/flexible-query-input/flexible-query-input.tsx
b/web-console/src/views/workbench-view/flexible-query-input/flexible-query-input.tsx
index 2417b64c944..7ba116e34b5 100644
---
a/web-console/src/views/workbench-view/flexible-query-input/flexible-query-input.tsx
+++
b/web-console/src/views/workbench-view/flexible-query-input/flexible-query-input.tsx
@@ -190,7 +190,7 @@ export class FlexibleQueryInput extends React.PureComponent<
const found = dedupe(findAllSqlQueriesInText(queryString), ({
startRowColumn }) =>
String(startRowColumn.row),
);
- if (found.length <= 1) return []; // Do not highlight a single query or no
queries
+ if (!found.length) return [];
// Do not report the first query if it is basically the main query minus
whitespace
const firstQuery = found[0].sql;
diff --git
a/web-console/src/views/workbench-view/result-table-pane/result-table-pane.tsx
b/web-console/src/views/workbench-view/result-table-pane/result-table-pane.tsx
index 492767b3eaf..294aeeeb798 100644
---
a/web-console/src/views/workbench-view/result-table-pane/result-table-pane.tsx
+++
b/web-console/src/views/workbench-view/result-table-pane/result-table-pane.tsx
@@ -546,7 +546,7 @@ export const ResultTablePane = React.memo(function
ResultTablePane(props: Result
? parsedQuery.getSelectExpressionForIndex(editingColumn)
: undefined;
- const numericColumnBraces = getNumericColumnBraces(queryResult, pagination);
+ const numericColumnBraces = getNumericColumnBraces(queryResult, undefined,
pagination);
return (
<div className={classNames('result-table-pane', { 'more-results':
hasMoreResults })}>
{finalPage ? (
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]