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 dc2ae1e99c Web console: improving the helper queries by allowing for
running inline helper queries (#14801)
dc2ae1e99c is described below
commit dc2ae1e99c79f08d29fcfa7d38d95908a9fe3225
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Wed Aug 16 23:50:43 2023 -0700
Web console: improving the helper queries by allowing for running inline
helper queries (#14801)
* remove helper queries
* fix tests
* take care of zero queries also
* switch to better place
---
web-console/e2e-tests/tutorial-batch.spec.ts | 2 +-
.../query-error-pane/query-error-pane.tsx | 6 +-
web-console/src/druid-models/index.ts | 1 -
.../workbench-query/workbench-query-part.ts | 261 ------------
.../workbench-query/workbench-query.spec.ts | 143 +------
.../workbench-query/workbench-query.ts | 402 ++++++-----------
.../src/singletons/ace-editor-state-cache.ts | 6 +-
.../src/singletons/execution-state-cache.ts | 6 -
.../src/singletons/workbench-running-promises.ts | 8 +-
web-console/src/utils/druid-query.spec.ts | 39 +-
web-console/src/utils/druid-query.ts | 38 +-
web-console/src/utils/general.spec.ts | 17 +-
web-console/src/utils/general.tsx | 14 +-
web-console/src/utils/query-cursor.ts | 11 +-
web-console/src/utils/sql.spec.ts | 349 +++++++++++++++
web-console/src/utils/sql.ts | 81 +++-
.../column-editor/column-editor.tsx | 1 -
.../expression-editor-dialog.tsx | 1 -
.../schema-step/schema-step.tsx | 6 +-
.../src/views/workbench-view/demo-queries.ts | 2 +-
.../execution-details-pane.tsx | 1 -
.../flexible-query-input.spec.tsx.snap | 2 +-
.../flexible-query-input/flexible-query-input.scss | 52 +++
.../flexible-query-input.spec.tsx | 6 +-
.../flexible-query-input/flexible-query-input.tsx | 167 ++++++--
.../workbench-view/helper-query/helper-query.scss | 100 -----
.../workbench-view/helper-query/helper-query.tsx | 473 ---------------------
.../views/workbench-view/query-tab/query-tab.scss | 41 +-
.../views/workbench-view/query-tab/query-tab.tsx | 123 ++----
.../views/workbench-view/run-panel/run-panel.tsx | 8 +-
.../src/views/workbench-view/workbench-view.tsx | 11 +-
31 files changed, 901 insertions(+), 1477 deletions(-)
diff --git a/web-console/e2e-tests/tutorial-batch.spec.ts
b/web-console/e2e-tests/tutorial-batch.spec.ts
index daae46a60f..4b4d90e200 100644
--- a/web-console/e2e-tests/tutorial-batch.spec.ts
+++ b/web-console/e2e-tests/tutorial-batch.spec.ts
@@ -36,7 +36,7 @@ import { waitTillWebConsoleReady } from './util/setup';
jest.setTimeout(5 * 60 * 1000);
-const ALL_SORTS_OF_CHARS = '<>|!@#$%^&`\'".,:;\\*()[]{}Россия 한국 中国!?~';
+const ALL_SORTS_OF_CHARS = '<>|!@#$%^&`\'".,:;\\*()[]{}Україна 한국 中国!?~';
describe('Tutorial: Loading a file', () => {
let browser: playwright.Browser;
diff --git a/web-console/src/components/query-error-pane/query-error-pane.tsx
b/web-console/src/components/query-error-pane/query-error-pane.tsx
index f8e0d3a622..284b58e21f 100644
--- a/web-console/src/components/query-error-pane/query-error-pane.tsx
+++ b/web-console/src/components/query-error-pane/query-error-pane.tsx
@@ -39,7 +39,7 @@ export const QueryErrorPane = React.memo(function
QueryErrorPane(props: QueryErr
return <div className="query-error-pane">{error.message}</div>;
}
- const { position, suggestion } = error;
+ const { startRowColumn, suggestion } = error;
let suggestionElement: JSX.Element | undefined;
if (suggestion && queryString && onQueryStringChange) {
const newQuery = suggestion.fn(queryString);
@@ -69,14 +69,14 @@ export const QueryErrorPane = React.memo(function
QueryErrorPane(props: QueryErr
)}
{error.errorMessageWithoutExpectation && (
<p>
- {position ? (
+ {startRowColumn ? (
<HighlightText
text={error.errorMessageWithoutExpectation}
find={/\(line \[\d+], column \[\d+]\)/}
replace={found => (
<a
onClick={() => {
- moveCursorTo(position);
+ moveCursorTo(startRowColumn);
}}
>
{found}
diff --git a/web-console/src/druid-models/index.ts
b/web-console/src/druid-models/index.ts
index 16edb184fc..18cd812c61 100644
--- a/web-console/src/druid-models/index.ts
+++ b/web-console/src/druid-models/index.ts
@@ -41,4 +41,3 @@ export * from './time/time';
export * from './timestamp-spec/timestamp-spec';
export * from './transform-spec/transform-spec';
export * from './workbench-query/workbench-query';
-export * from './workbench-query/workbench-query-part';
diff --git
a/web-console/src/druid-models/workbench-query/workbench-query-part.ts
b/web-console/src/druid-models/workbench-query/workbench-query-part.ts
deleted file mode 100644
index 45fce5914b..0000000000
--- a/web-console/src/druid-models/workbench-query/workbench-query-part.ts
+++ /dev/null
@@ -1,261 +0,0 @@
-/*
- * 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 type { SqlValues, SqlWithQuery } from '@druid-toolkit/query';
-import { SqlExpression, SqlQuery, T } from '@druid-toolkit/query';
-import Hjson from 'hjson';
-import * as JSONBig from 'json-bigint-native';
-
-import type { ColumnMetadata } from '../../utils';
-import { compact, filterMap, generate8HexId } from '../../utils';
-import type { LastExecution } from '../execution/execution';
-import { validateLastExecution } from '../execution/execution';
-import { fitExternalConfigPattern } from '../external-config/external-config';
-
-// -----------------------------
-
-export interface WorkbenchQueryPartValue {
- id: string;
- queryName?: string;
- queryString: string;
- collapsed?: boolean;
- lastExecution?: LastExecution;
-}
-
-export class WorkbenchQueryPart {
- static blank() {
- return new WorkbenchQueryPart({
- id: generate8HexId(),
- queryString: '',
- });
- }
-
- static fromQuery(query: SqlQuery | SqlValues, queryName?: string,
collapsed?: boolean) {
- return this.fromQueryString(query.changeParens([]).toString(), queryName,
collapsed);
- }
-
- static fromQueryString(queryString: string, queryName?: string, collapsed?:
boolean) {
- return new WorkbenchQueryPart({
- id: generate8HexId(),
- queryName,
- queryString,
- collapsed,
- });
- }
-
- static isTaskEngineNeeded(queryString: string): boolean {
- return /EXTERN\s*\(|(?:INSERT|REPLACE)\s+INTO/im.test(queryString);
- }
-
- static getIngestDatasourceFromQueryFragment(queryFragment: string): string |
undefined {
- // Assuming the queryFragment is no parsable find the prefix that look
like:
- // REPLACE<space>INTO<space><whatever><space>SELECT<space or EOF>
- const matchInsertReplaceIndex =
queryFragment.match(/(?:INSERT|REPLACE)\s+INTO/i)?.index;
- if (typeof matchInsertReplaceIndex !== 'number') return;
-
- const queryStartingWithInsertOrReplace =
queryFragment.substring(matchInsertReplaceIndex);
-
- const matchEnd =
queryStartingWithInsertOrReplace.match(/\b(?:SELECT|WITH)\b|$/i);
- const fragmentQuery = SqlQuery.maybeParse(
- queryStartingWithInsertOrReplace.substring(0, matchEnd?.index) + '
SELECT * FROM t',
- );
- if (!fragmentQuery) return;
-
- return fragmentQuery.getIngestTable()?.getName();
- }
-
- public readonly id: string;
- public readonly queryName?: string;
- public readonly queryString: string;
- public readonly collapsed: boolean;
- public readonly lastExecution?: LastExecution;
-
- public readonly parsedQuery?: SqlQuery;
-
- constructor(value: WorkbenchQueryPartValue) {
- this.id = value.id;
- this.queryName = value.queryName;
- this.queryString = value.queryString;
- this.collapsed = Boolean(value.collapsed);
- this.lastExecution = validateLastExecution(value.lastExecution);
-
- try {
- this.parsedQuery = SqlQuery.parse(this.queryString);
- } catch {}
- }
-
- public valueOf(): WorkbenchQueryPartValue {
- return {
- id: this.id,
- queryName: this.queryName,
- queryString: this.queryString,
- collapsed: this.collapsed,
- lastExecution: this.lastExecution,
- };
- }
-
- public changeId(id: string): WorkbenchQueryPart {
- return new WorkbenchQueryPart({ ...this.valueOf(), id });
- }
-
- public changeQueryName(queryName: string): WorkbenchQueryPart {
- return new WorkbenchQueryPart({ ...this.valueOf(), queryName });
- }
-
- public changeQueryString(queryString: string): WorkbenchQueryPart {
- return new WorkbenchQueryPart({ ...this.valueOf(), queryString });
- }
-
- public changeCollapsed(collapsed: boolean): WorkbenchQueryPart {
- return new WorkbenchQueryPart({ ...this.valueOf(), collapsed });
- }
-
- public changeLastExecution(lastExecution: LastExecution | undefined):
WorkbenchQueryPart {
- return new WorkbenchQueryPart({ ...this.valueOf(), lastExecution });
- }
-
- public clear(): WorkbenchQueryPart {
- return new WorkbenchQueryPart({
- ...this.valueOf(),
- queryString: '',
- });
- }
-
- public isEmptyQuery(): boolean {
- return this.queryString.trim() === '';
- }
-
- public isJsonLike(): boolean {
- return this.queryString.trim().startsWith('{');
- }
-
- public issueWithJson(): string | undefined {
- try {
- Hjson.parse(this.queryString);
- } catch (e) {
- return e.message;
- }
- return;
- }
-
- public isSqlInJson(): boolean {
- try {
- const query = Hjson.parse(this.queryString);
- return typeof query.query === 'string';
- } catch {
- return false;
- }
- }
-
- public getSqlString(): string {
- if (this.isJsonLike()) {
- const query = Hjson.parse(this.queryString);
- return typeof query.query === 'string' ? query.query : '';
- } else {
- return this.queryString;
- }
- }
-
- public prettyPrintJson(): WorkbenchQueryPart {
- let parsed: unknown;
- try {
- parsed = Hjson.parse(this.queryString);
- } catch {
- return this;
- }
- return this.changeQueryString(JSONBig.stringify(parsed, undefined, 2));
- }
-
- public getIngestDatasource(): string | undefined {
- const { queryString, parsedQuery } = this;
- if (parsedQuery) {
- return parsedQuery.getIngestTable()?.getName();
- }
-
- if (this.isJsonLike()) return;
-
- return
WorkbenchQueryPart.getIngestDatasourceFromQueryFragment(queryString);
- }
-
- public getInlineMetadata(): ColumnMetadata[] {
- const { queryName, parsedQuery } = this;
- if (queryName && parsedQuery) {
- try {
- return
fitExternalConfigPattern(parsedQuery).signature.map(columnDeclaration => ({
- COLUMN_NAME: columnDeclaration.getColumnName(),
- DATA_TYPE: columnDeclaration.columnType.getEffectiveType(),
- TABLE_NAME: queryName,
- TABLE_SCHEMA: 'druid',
- }));
- } catch {
- return filterMap(parsedQuery.getSelectExpressionsArray(), ex => {
- const outputName = ex.getOutputName();
- if (!outputName) return;
- return {
- COLUMN_NAME: outputName,
- DATA_TYPE: 'UNKNOWN',
- TABLE_NAME: queryName,
- TABLE_SCHEMA: 'druid',
- };
- });
- }
- }
- return [];
- }
-
- public isTaskEngineNeeded(): boolean {
- return WorkbenchQueryPart.isTaskEngineNeeded(this.queryString);
- }
-
- public extractCteHelpers(): WorkbenchQueryPart[] | undefined {
- let flatQuery: SqlQuery;
- try {
- // We need to do our own parsing here because this.parseQuery
necessarily must be a SqlQuery
- // object, and we might have a SqlWithQuery here.
- flatQuery = (SqlExpression.parse(this.queryString) as
SqlWithQuery).flattenWith();
- } catch {
- return;
- }
-
- const possibleNewParts = flatQuery.getWithParts().map(({ table, columns,
query }) => {
- if (columns) return;
- return WorkbenchQueryPart.fromQuery(query, table.name, true);
- });
- if (!possibleNewParts.length) return;
-
- const newParts = compact(possibleNewParts);
- if (newParts.length !== possibleNewParts.length) return;
-
- return
newParts.concat(this.changeQueryString(flatQuery.changeWithParts(undefined).toString()));
- }
-
- public toWithPart(): string {
- const { queryName, queryString } = this;
- return `${T(queryName || 'q')} AS (\n${queryString}\n)`;
- }
-
- public duplicate(): WorkbenchQueryPart {
- return this.changeId(generate8HexId()).changeLastExecution(undefined);
- }
-
- public addPreviewLimit(): WorkbenchQueryPart {
- const { parsedQuery } = this;
- if (!parsedQuery || parsedQuery.hasLimit()) return this;
- return
this.changeQueryString(parsedQuery.changeLimitValue(10000).toString());
- }
-}
diff --git
a/web-console/src/druid-models/workbench-query/workbench-query.spec.ts
b/web-console/src/druid-models/workbench-query/workbench-query.spec.ts
index bb1fa9c955..8732b93d42 100644
--- a/web-console/src/druid-models/workbench-query/workbench-query.spec.ts
+++ b/web-console/src/druid-models/workbench-query/workbench-query.spec.ts
@@ -19,7 +19,6 @@
import { sane } from '@druid-toolkit/query';
import { WorkbenchQuery } from './workbench-query';
-import { WorkbenchQueryPart } from './workbench-query-part';
describe('WorkbenchQuery', () => {
beforeAll(() => {
@@ -107,12 +106,6 @@ describe('WorkbenchQuery', () => {
describe('.fromString', () => {
const tabString = sane`
- ===== Helper: q =====
-
- SELECT *
-
- FROM wikipedia
-
===== Query =====
SELECT * FROM q
@@ -207,6 +200,7 @@ describe('WorkbenchQuery', () => {
expect(apiQuery).toEqual({
cancelQueryId: 'deadbeef-9fb0-499c-8475-ea461e96a4fd',
engine: 'native',
+ prefixLines: 0,
query: {
aggregations: [
{
@@ -258,6 +252,7 @@ describe('WorkbenchQuery', () => {
expect(apiQuery).toEqual({
cancelQueryId: 'lol',
engine: 'native',
+ prefixLines: 0,
query: {
aggregations: [
{
@@ -302,7 +297,7 @@ describe('WorkbenchQuery', () => {
sqlTypesHeader: true,
typesHeader: true,
},
- sqlPrefixLines: 0,
+ prefixLines: 0,
});
});
@@ -328,7 +323,7 @@ describe('WorkbenchQuery', () => {
sqlTypesHeader: true,
typesHeader: true,
},
- sqlPrefixLines: 0,
+ prefixLines: 0,
});
});
@@ -368,7 +363,7 @@ describe('WorkbenchQuery', () => {
sqlTypesHeader: true,
typesHeader: true,
},
- sqlPrefixLines: 0,
+ prefixLines: 0,
});
});
@@ -407,7 +402,7 @@ describe('WorkbenchQuery', () => {
sqlTypesHeader: true,
typesHeader: true,
},
- sqlPrefixLines: 0,
+ prefixLines: 0,
});
});
@@ -435,7 +430,7 @@ describe('WorkbenchQuery', () => {
sqlTypesHeader: true,
typesHeader: true,
},
- sqlPrefixLines: 0,
+ prefixLines: 0,
});
});
@@ -536,130 +531,6 @@ describe('WorkbenchQuery', () => {
});
});
- describe('#extractCteHelpers', () => {
- it('works', () => {
- const sql = sane`
- REPLACE INTO task_statuses OVERWRITE ALL
- WITH
- task_statuses AS (
- SELECT * FROM
- TABLE(
- EXTERN(
-
'{"type":"local","baseDir":"/Users/vadim/Desktop/","filter":"task_statuses.json"}',
- '{"type":"json"}',
-
'[{"name":"id","type":"string"},{"name":"status","type":"string"},{"name":"duration","type":"long"},{"name":"errorMsg","type":"string"},{"name":"created_date","type":"string"}]'
- )
- )
- )
- (
- --PLACE INTO task_statuses OVERWRITE ALL
- SELECT
- id,
- status,
- duration,
- errorMsg,
- created_date
- FROM task_statuses
- --RTITIONED BY ALL
- )
- PARTITIONED BY ALL
- `;
-
-
expect(WorkbenchQuery.blank().changeQueryString(sql).extractCteHelpers().getQueryString())
- .toEqual(sane`
- REPLACE INTO task_statuses OVERWRITE ALL
- SELECT
- id,
- status,
- duration,
- errorMsg,
- created_date
- FROM task_statuses
- PARTITIONED BY ALL
- `);
- });
- });
-
- describe('#materializeHelpers', () => {
- it('works', () => {
- expect(
- WorkbenchQuery.blank()
- .changeQueryParts([
- new WorkbenchQueryPart({
- id: 'aaa',
- queryName: 'kttm_data',
- queryString: sane`
- SELECT * FROM TABLE(
- EXTERN(
-
'{"type":"http","uris":["https://static.imply.io/example-data/kttm-v2/kttm-v2-2019-08-25.json.gz"]}',
- '{"type":"json"}'
- )
- ) EXTEND ("timestamp" VARCHAR, "agent_type" VARCHAR)
- `,
- }),
- new WorkbenchQueryPart({
- id: 'bbb',
- queryName: 'country_lookup',
- queryString: sane`
- SELECT * FROM TABLE(
- EXTERN(
-
'{"type":"http","uris":["https://static.imply.io/example-data/lookup/countries.tsv"]}',
- '{"type":"tsv","findColumnsFromHeader":true}'
- )
- ) EXTEND ("Country" VARCHAR, "Capital" VARCHAR, "ISO3"
VARCHAR, "ISO2" VARCHAR))
- `,
- }),
- new WorkbenchQueryPart({
- id: 'ccc',
- queryName: 'x',
- queryString: sane`
- SELECT
- os,
- CONCAT(country, ' (', country_lookup.ISO3, ')') AS "country",
- COUNT(DISTINCT session) AS "unique_sessions"
- FROM kttm_data
- LEFT JOIN country_lookup ON country_lookup.Country =
kttm_data.country
- GROUP BY 1, 2
- ORDER BY 3 DESC
- LIMIT 10
- `,
- }),
- ])
- .materializeHelpers()
- .getQueryString(),
- ).toEqual(sane`
- WITH
- "kttm_data" AS (
- SELECT * FROM TABLE(
- EXTERN(
-
'{"type":"http","uris":["https://static.imply.io/example-data/kttm-v2/kttm-v2-2019-08-25.json.gz"]}',
- '{"type":"json"}'
- )
- ) EXTEND ("timestamp" VARCHAR, "agent_type" VARCHAR)
- ),
- "country_lookup" AS (
- SELECT * FROM TABLE(
- EXTERN(
-
'{"type":"http","uris":["https://static.imply.io/example-data/lookup/countries.tsv"]}',
- '{"type":"tsv","findColumnsFromHeader":true}'
- )
- ) EXTEND ("Country" VARCHAR, "Capital" VARCHAR, "ISO3" VARCHAR, "ISO2"
VARCHAR))
- )
- (
- SELECT
- os,
- CONCAT(country, ' (', country_lookup.ISO3, ')') AS "country",
- COUNT(DISTINCT session) AS "unique_sessions"
- FROM kttm_data
- LEFT JOIN country_lookup ON country_lookup.Country = kttm_data.country
- GROUP BY 1, 2
- ORDER BY 3 DESC
- LIMIT 10
- )
- `);
- });
- });
-
describe('#getIssue', () => {
it('works', () => {
expect(
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 bee9f07e03..874b10165a 100644
--- a/web-console/src/druid-models/workbench-query/workbench-query.ts
+++ b/web-console/src/druid-models/workbench-query/workbench-query.ts
@@ -20,7 +20,6 @@ import type {
SqlClusteredByClause,
SqlExpression,
SqlPartitionedByClause,
- SqlQuery,
} from '@druid-toolkit/query';
import {
C,
@@ -28,17 +27,18 @@ import {
SqlLiteral,
SqlOrderByClause,
SqlOrderByExpression,
- SqlTable,
+ SqlQuery,
} from '@druid-toolkit/query';
import Hjson from 'hjson';
import * as JSONBig from 'json-bigint-native';
import { v4 as uuidv4 } from 'uuid';
-import type { ColumnMetadata, RowColumn } from '../../utils';
-import { deleteKeys, generate8HexId } from '../../utils';
+import type { RowColumn } from '../../utils';
+import { deleteKeys } from '../../utils';
import type { DruidEngine } from '../druid-engine/druid-engine';
import { validDruidEngine } from '../druid-engine/druid-engine';
import type { LastExecution } from '../execution/execution';
+import { validateLastExecution } from '../execution/execution';
import type { ExternalConfig } from '../external-config/external-config';
import {
externalConfigToIngestQueryPattern,
@@ -46,7 +46,7 @@ import {
} from '../ingest-query-pattern/ingest-query-pattern';
import type { QueryContext } from '../query-context/query-context';
-import { WorkbenchQueryPart } from './workbench-query-part';
+const ISSUE_MARKER = '--:ISSUE:';
export interface TabEntry {
id: string;
@@ -64,10 +64,15 @@ interface IngestionLines {
// -----------------------------
export interface WorkbenchQueryValue {
- queryParts: WorkbenchQueryPart[];
+ queryString: string;
queryContext: QueryContext;
engine?: DruidEngine;
+ lastExecution?: LastExecution;
unlimited?: boolean;
+ prefixLines?: number;
+
+ // Legacy
+ queryParts?: any[];
}
export class WorkbenchQuery {
@@ -75,8 +80,8 @@ export class WorkbenchQuery {
static blank(): WorkbenchQuery {
return new WorkbenchQuery({
+ queryString: '',
queryContext: {},
- queryParts: [WorkbenchQueryPart.blank()],
});
}
@@ -87,19 +92,15 @@ export class WorkbenchQuery {
partitionedByHint: string | undefined,
): WorkbenchQuery {
return new WorkbenchQuery({
- queryContext: {},
- queryParts: [
- WorkbenchQueryPart.fromQueryString(
- ingestQueryPatternToQuery(
- externalConfigToIngestQueryPattern(
- externalConfig,
- isArrays,
- timeExpression,
- partitionedByHint,
- ),
- ).toString(),
+ queryString: ingestQueryPatternToQuery(
+ externalConfigToIngestQueryPattern(
+ externalConfig,
+ isArrays,
+ timeExpression,
+ partitionedByHint,
),
- ],
+ ).toString(),
+ queryContext: {},
});
}
@@ -118,39 +119,21 @@ export class WorkbenchQuery {
}
}
- const queryParts: WorkbenchQueryPart[] = [];
+ let queryString = '';
let queryContext: QueryContext = {};
for (let i = 0; i < headers.length; i++) {
const header = headers[i];
const body = bodies[i];
if (header === 'Context') {
queryContext = JSONBig.parse(body);
- } else if (header.startsWith('Helper:')) {
- queryParts.push(
- new WorkbenchQueryPart({
- id: generate8HexId(),
- queryName: header.replace(/^Helper:/, '').trim(),
- queryString: body,
- collapsed: true,
- }),
- );
} else {
- queryParts.push(
- new WorkbenchQueryPart({
- id: generate8HexId(),
- queryString: body,
- }),
- );
+ queryString = body;
}
}
- if (!queryParts.length) {
- queryParts.push(WorkbenchQueryPart.blank());
- }
-
return new WorkbenchQuery({
+ queryString,
queryContext,
- queryParts,
});
}
@@ -229,20 +212,44 @@ export class WorkbenchQuery {
return { row: Number(m[1]) - 1, column: Number(m[2]) - 1 };
}
- public readonly queryParts: WorkbenchQueryPart[];
+ static isTaskEngineNeeded(queryString: string): boolean {
+ return /EXTERN\s*\(|(?:INSERT|REPLACE)\s+INTO/im.test(queryString);
+ }
+
+ static getIngestDatasourceFromQueryFragment(queryFragment: string): string |
undefined {
+ // Assuming the queryFragment is no parsable find the prefix that look
like:
+ // REPLACE<space>INTO<space><whatever><space>SELECT<space or EOF>
+ const matchInsertReplaceIndex =
queryFragment.match(/(?:INSERT|REPLACE)\s+INTO/i)?.index;
+ if (typeof matchInsertReplaceIndex !== 'number') return;
+
+ const queryStartingWithInsertOrReplace =
queryFragment.substring(matchInsertReplaceIndex);
+
+ const matchEnd =
queryStartingWithInsertOrReplace.match(/\b(?:SELECT|WITH)\b|$/i);
+ const fragmentQuery = SqlQuery.maybeParse(
+ queryStartingWithInsertOrReplace.substring(0, matchEnd?.index) + '
SELECT * FROM t',
+ );
+ if (!fragmentQuery) return;
+
+ return fragmentQuery.getIngestTable()?.getName();
+ }
+
+ public readonly queryString: string;
public readonly queryContext: QueryContext;
public readonly engine?: DruidEngine;
+ public readonly lastExecution?: LastExecution;
public readonly unlimited?: boolean;
+ public readonly prefixLines?: number;
+
+ public readonly parsedQuery?: SqlQuery;
constructor(value: WorkbenchQueryValue) {
- let queryParts = value.queryParts;
- if (!Array.isArray(queryParts) || !queryParts.length) {
- queryParts = [WorkbenchQueryPart.blank()];
+ let queryString = value.queryString;
+ // Back compat to read legacy workbench query
+ if (typeof queryString === 'undefined' && Array.isArray(value.queryParts))
{
+ const lastQueryPart = value.queryParts[value.queryParts.length - 1];
+ queryString = lastQueryPart.queryString || '';
}
- if (!(queryParts instanceof WorkbenchQueryPart)) {
- queryParts = queryParts.map(p => new WorkbenchQueryPart(p));
- }
- this.queryParts = queryParts;
+ this.queryString = queryString;
this.queryContext = value.queryContext;
// Start back compat code for the engine names that might be coming from
local storage
@@ -255,13 +262,17 @@ export class WorkbenchQuery {
// End bac compat code
this.engine = validDruidEngine(possibleEngine) ? possibleEngine :
undefined;
+ this.lastExecution = validateLastExecution(value.lastExecution);
if (value.unlimited) this.unlimited = true;
+ this.prefixLines = value.prefixLines;
+
+ this.parsedQuery = SqlQuery.maybeParse(this.queryString);
}
public valueOf(): WorkbenchQueryValue {
return {
- queryParts: this.queryParts,
+ queryString: this.queryString,
queryContext: this.queryContext,
engine: this.engine,
unlimited: this.unlimited,
@@ -269,21 +280,17 @@ export class WorkbenchQuery {
}
public toString(): string {
- const { queryParts, queryContext } = this;
- return queryParts
- .slice(0, queryParts.length - 1)
- .flatMap(part => [`===== Helper: ${part.queryName} =====`,
part.queryString])
- .concat([
- `===== Query =====`,
- this.getLastPart().queryString,
- `===== Context =====`,
- JSONBig.stringify(queryContext, undefined, 2),
- ])
- .join('\n\n');
+ const { queryString, queryContext } = this;
+ return [
+ `===== Query =====`,
+ queryString,
+ `===== Context =====`,
+ JSONBig.stringify(queryContext, undefined, 2),
+ ].join('\n\n');
}
- public changeQueryParts(queryParts: WorkbenchQueryPart[]): WorkbenchQuery {
- return new WorkbenchQuery({ ...this.valueOf(), queryParts });
+ public changeQueryString(queryString: string): WorkbenchQuery {
+ return new WorkbenchQuery({ ...this.valueOf(), queryString });
}
public changeQueryContext(queryContext: QueryContext): WorkbenchQuery {
@@ -294,20 +301,28 @@ export class WorkbenchQuery {
return new WorkbenchQuery({ ...this.valueOf(), engine });
}
+ public changeLastExecution(lastExecution: LastExecution | undefined):
WorkbenchQuery {
+ return new WorkbenchQuery({ ...this.valueOf(), lastExecution });
+ }
+
public changeUnlimited(unlimited: boolean): WorkbenchQuery {
return new WorkbenchQuery({ ...this.valueOf(), unlimited });
}
+ public changePrefixLines(prefixLines: number): WorkbenchQuery {
+ return new WorkbenchQuery({ ...this.valueOf(), prefixLines });
+ }
+
public isTaskEngineNeeded(): boolean {
- return this.queryParts.some(part => part.isTaskEngineNeeded());
+ return WorkbenchQuery.isTaskEngineNeeded(this.queryString);
}
public getEffectiveEngine(): DruidEngine {
const { engine } = this;
if (engine) return engine;
const enabledEngines = WorkbenchQuery.getQueryEngines();
- if (this.getLastPart().isJsonLike()) {
- if (this.getLastPart().isSqlInJson()) {
+ if (this.isJsonLike()) {
+ if (this.isSqlInJson()) {
if (enabledEngines.includes('sql-native')) return 'sql-native';
} else {
if (enabledEngines.includes('native')) return 'native';
@@ -318,61 +333,60 @@ export class WorkbenchQuery {
return enabledEngines[0] || 'sql-native';
}
- private getLastPart(): WorkbenchQueryPart {
- const { queryParts } = this;
- return queryParts[queryParts.length - 1];
- }
-
- public getId(): string {
- return this.getLastPart().id;
- }
-
- public getIds(): string[] {
- return this.queryParts.map(queryPart => queryPart.id);
- }
-
- public getQueryName(): string {
- return this.getLastPart().queryName || '';
- }
-
public getQueryString(): string {
- return this.getLastPart().queryString;
- }
-
- public getCollapsed(): boolean {
- return this.getLastPart().collapsed;
+ return this.queryString;
}
public getLastExecution(): LastExecution | undefined {
- return this.getLastPart().lastExecution;
+ return this.lastExecution;
}
public getParsedQuery(): SqlQuery | undefined {
- return this.getLastPart().parsedQuery;
+ return this.parsedQuery;
}
public isEmptyQuery(): boolean {
- return this.getLastPart().isEmptyQuery();
+ return this.queryString.trim() === '';
}
public getIssue(): string | undefined {
- const lastPart = this.getLastPart();
- if (lastPart.isJsonLike()) {
- return lastPart.issueWithJson();
+ if (this.isJsonLike()) {
+ return this.issueWithJson();
+ }
+ return;
+ }
+
+ public isJsonLike(): boolean {
+ return this.queryString.trim().startsWith('{');
+ }
+
+ public issueWithJson(): string | undefined {
+ try {
+ Hjson.parse(this.queryString);
+ } catch (e) {
+ return e.message;
}
return;
}
+ public isSqlInJson(): boolean {
+ try {
+ const query = Hjson.parse(this.queryString);
+ return typeof query.query === 'string';
+ } catch {
+ return false;
+ }
+ }
+
public canPrettify(): boolean {
- const lastPart = this.getLastPart();
- return lastPart.isJsonLike();
+ return this.isJsonLike();
}
public prettify(): WorkbenchQuery {
- const lastPart = this.getLastPart();
+ const queryString = this.getQueryString();
let parsed;
try {
- parsed = Hjson.parse(lastPart.queryString);
+ parsed = Hjson.parse(queryString);
} catch {
return this;
}
@@ -381,39 +395,19 @@ export class WorkbenchQuery {
public getIngestDatasource(): string | undefined {
if (this.getEffectiveEngine() !== 'sql-msq-task') return;
- return this.getLastPart().getIngestDatasource();
- }
-
- public isIngestQuery(): boolean {
- return Boolean(this.getIngestDatasource());
- }
- private changeLastQueryPart(lastQueryPart: WorkbenchQueryPart):
WorkbenchQuery {
- const { queryParts } = this;
- return this.changeQueryParts(queryParts.slice(0, queryParts.length -
1).concat(lastQueryPart));
- }
-
- public changeQueryName(queryName: string): WorkbenchQuery {
- return
this.changeLastQueryPart(this.getLastPart().changeQueryName(queryName));
- }
-
- public changeQueryString(queryString: string): WorkbenchQuery {
- return
this.changeLastQueryPart(this.getLastPart().changeQueryString(queryString));
- }
+ const { queryString, parsedQuery } = this;
+ if (parsedQuery) {
+ return parsedQuery.getIngestTable()?.getName();
+ }
- public changeCollapsed(collapsed: boolean): WorkbenchQuery {
- return
this.changeLastQueryPart(this.getLastPart().changeCollapsed(collapsed));
- }
+ if (this.isJsonLike()) return;
- public changeLastExecution(lastExecution: LastExecution | undefined):
WorkbenchQuery {
- return
this.changeLastQueryPart(this.getLastPart().changeLastExecution(lastExecution));
+ return WorkbenchQuery.getIngestDatasourceFromQueryFragment(queryString);
}
- public clear(): WorkbenchQuery {
- return new WorkbenchQuery({
- queryParts: [],
- queryContext: {},
- });
+ public isIngestQuery(): boolean {
+ return Boolean(this.getIngestDatasource());
}
public toggleUnlimited(): WorkbenchQuery {
@@ -421,56 +415,11 @@ export class WorkbenchQuery {
return this.changeUnlimited(!unlimited);
}
- public hasHelperQueries(): boolean {
- return this.queryParts.length > 1;
- }
-
- public materializeHelpers(): WorkbenchQuery {
- if (!this.hasHelperQueries()) return this;
- const { query } = this.getApiQuery();
- const queryString = query.query;
- if (typeof queryString !== 'string') return this;
- const lastPart = this.getLastPart();
- return this.changeQueryParts([
- new WorkbenchQueryPart({
- id: lastPart.id,
- queryName: lastPart.queryName,
- queryString,
- }),
- ]);
- }
-
- public extractCteHelpers(): WorkbenchQuery {
- const { queryParts } = this;
-
- let changed = false;
- const newParts = queryParts.flatMap(queryPart => {
- const helpers = queryPart.extractCteHelpers();
- if (helpers) changed = true;
- return helpers || [queryPart];
- });
- return changed ? this.changeQueryParts(newParts) : this;
- }
-
public makePreview(): WorkbenchQuery {
if (!this.isIngestQuery()) return this;
let ret: WorkbenchQuery = this;
- // Limit all the helper queries
- const parsedQuery = this.getParsedQuery();
- if (parsedQuery) {
- const fromExpression = parsedQuery.getFirstFromExpression();
- if (fromExpression instanceof SqlTable) {
- const firstTable = fromExpression.getName();
- ret = ret.changeQueryParts(
- this.queryParts.map(queryPart =>
- queryPart.queryName === firstTable ? queryPart.addPreviewLimit() :
queryPart,
- ),
- );
- }
- }
-
// Explicitly select MSQ, adjust the context, set maxNumTasks to the
lowest possible and add in ingest mode flags
ret = ret.changeEngine('sql-msq-task').changeQueryContext({
...this.queryContext,
@@ -480,8 +429,8 @@ export class WorkbenchQuery {
});
// Remove everything pertaining to INSERT INTO / REPLACE INTO from the
query string
- const newQueryString = parsedQuery
- ? parsedQuery
+ const newQueryString = this.parsedQuery
+ ? this.parsedQuery
.changeInsertClause(undefined)
.changeReplaceClause(undefined)
.changePartitionedByClause(undefined)
@@ -502,18 +451,16 @@ export class WorkbenchQuery {
public getApiQuery(makeQueryId: () => string = uuidv4): {
engine: DruidEngine;
query: Record<string, any>;
- sqlPrefixLines?: number;
+ prefixLines: number;
cancelQueryId?: string;
} {
- const { queryParts, queryContext, unlimited } = this;
- if (!queryParts.length) throw new Error(`should not get here`);
+ const { queryString, queryContext, unlimited, prefixLines } = this;
const engine = this.getEffectiveEngine();
- const lastQueryPart = this.getLastPart();
if (engine === 'native') {
let query: any;
try {
- query = Hjson.parse(lastQueryPart.queryString);
+ query = Hjson.parse(queryString);
} catch (e) {
throw new Error(
`You have selected the 'native' engine but the query you entered
could not be parsed as JSON: ${e.message}`,
@@ -530,24 +477,21 @@ export class WorkbenchQuery {
return {
engine,
query,
+ prefixLines: prefixLines || 0,
cancelQueryId,
};
}
- const prefixParts = queryParts
- .slice(0, queryParts.length - 1)
- .filter(part => !part.getIngestDatasource());
-
let apiQuery: Record<string, any> = {};
- if (lastQueryPart.isJsonLike()) {
+ if (this.isJsonLike()) {
try {
- apiQuery = Hjson.parse(lastQueryPart.queryString);
+ apiQuery = Hjson.parse(queryString);
} catch (e) {
throw new Error(`The query you entered could not be parsed as JSON:
${e.message}`);
}
} else {
apiQuery = {
- query: lastQueryPart.queryString,
+ query: queryString,
resultFormat: 'array',
header: true,
typesHeader: true,
@@ -555,42 +499,13 @@ export class WorkbenchQuery {
};
}
- let queryPrepend = '';
- let queryAppend = '';
-
- if (prefixParts.length) {
- const { insertReplaceLine, overwriteLine, partitionedByLine,
clusteredByLine } =
- WorkbenchQuery.getIngestionLines(apiQuery.query);
- if (insertReplaceLine) {
- queryPrepend += insertReplaceLine + '\n';
- if (overwriteLine) {
- queryPrepend += overwriteLine + '\n';
- }
-
- apiQuery.query = WorkbenchQuery.commentOutIngestParts(apiQuery.query);
-
- if (clusteredByLine) {
- queryAppend = '\n' + clusteredByLine + queryAppend;
- }
- if (partitionedByLine) {
- queryAppend = '\n' + partitionedByLine + queryAppend;
- }
- }
-
- queryPrepend += 'WITH\n' + prefixParts.map(p =>
p.toWithPart()).join(',\n') + '\n(\n';
- queryAppend = '\n)' + queryAppend;
- }
-
- let prefixLines = 0;
- if (queryPrepend) {
- prefixLines = queryPrepend.split('\n').length - 1;
- apiQuery.query = queryPrepend + apiQuery.query + queryAppend;
- }
-
- const m = /--:ISSUE:(.+)(?:\n|$)/.exec(apiQuery.query);
- if (m) {
+ const issueIndex = String(apiQuery.query).indexOf(ISSUE_MARKER);
+ if (issueIndex !== -1) {
+ const issueComment = String(apiQuery.query)
+ .slice(issueIndex + ISSUE_MARKER.length)
+ .split('\n')[0];
throw new Error(
- `This query contains an ISSUE comment: ${m[1]
+ `This query contains an ISSUE comment: ${issueComment
.trim()
.replace(
/\.$/,
@@ -628,55 +543,8 @@ export class WorkbenchQuery {
return {
engine,
query: apiQuery,
- sqlPrefixLines: prefixLines,
+ prefixLines: prefixLines || 0,
cancelQueryId,
};
}
-
- public getInlineMetadata(): ColumnMetadata[] {
- const { queryParts } = this;
- if (!queryParts.length) return [];
- return queryParts.slice(0, queryParts.length - 1).flatMap(p =>
p.getInlineMetadata());
- }
-
- public getPrefix(index: number): WorkbenchQuery {
- return this.changeQueryParts(this.queryParts.slice(0, index + 1));
- }
-
- public getPrefixQueries(): WorkbenchQuery[] {
- return this.queryParts.slice(0, this.queryParts.length - 1).map((_, i) =>
this.getPrefix(i));
- }
-
- public applyUpdate(newQuery: WorkbenchQuery, index: number): WorkbenchQuery {
- return
newQuery.changeQueryParts(newQuery.queryParts.concat(this.queryParts.slice(index
+ 1)));
- }
-
- public duplicate(): WorkbenchQuery {
- return this.changeQueryParts(this.queryParts.map(part =>
part.duplicate()));
- }
-
- public duplicateLast(): WorkbenchQuery {
- const { queryParts } = this;
- const last = this.getLastPart();
- return this.changeQueryParts(queryParts.concat(last.duplicate()));
- }
-
- public addBlank(): WorkbenchQuery {
- const { queryParts } = this;
- const last = this.getLastPart();
- return this.changeQueryParts(
- queryParts.slice(0, queryParts.length - 1).concat(
- last
- .changeQueryName(last.queryName || 'q')
- .changeCollapsed(true)
- .changeLastExecution(undefined),
- WorkbenchQueryPart.blank(),
- ),
- );
- }
-
- public remove(index: number): WorkbenchQuery {
- const { queryParts } = this;
- return this.changeQueryParts(queryParts.filter((_, i) => i !== index));
- }
}
diff --git a/web-console/src/singletons/ace-editor-state-cache.ts
b/web-console/src/singletons/ace-editor-state-cache.ts
index 0a81173a19..9e1b73aaba 100644
--- a/web-console/src/singletons/ace-editor-state-cache.ts
+++ b/web-console/src/singletons/ace-editor-state-cache.ts
@@ -40,9 +40,7 @@ export class AceEditorStateCache {
session.setUndoManager(state.undoManager);
}
- static deleteStates(ids: string[]): void {
- for (const id of ids) {
- delete AceEditorStateCache.states[id];
- }
+ static deleteState(id: string): void {
+ delete AceEditorStateCache.states[id];
}
}
diff --git a/web-console/src/singletons/execution-state-cache.ts
b/web-console/src/singletons/execution-state-cache.ts
index 233ed23f7b..ef1b2d60ae 100644
--- a/web-console/src/singletons/execution-state-cache.ts
+++ b/web-console/src/singletons/execution-state-cache.ts
@@ -33,10 +33,4 @@ export class ExecutionStateCache {
static deleteState(id: string): void {
delete ExecutionStateCache.cache[id];
}
-
- static deleteStates(ids: string[]): void {
- for (const id of ids) {
- delete ExecutionStateCache.cache[id];
- }
- }
}
diff --git a/web-console/src/singletons/workbench-running-promises.ts
b/web-console/src/singletons/workbench-running-promises.ts
index 7f390b2284..5a14ec136b 100644
--- a/web-console/src/singletons/workbench-running-promises.ts
+++ b/web-console/src/singletons/workbench-running-promises.ts
@@ -20,7 +20,7 @@ import type { QueryResult } from '@druid-toolkit/query';
export interface WorkbenchRunningPromise {
promise: Promise<QueryResult>;
- sqlPrefixLines: number | undefined;
+ prefixLines: number;
}
export class WorkbenchRunningPromises {
@@ -41,10 +41,4 @@ export class WorkbenchRunningPromises {
static deletePromise(id: string): void {
delete WorkbenchRunningPromises.promises[id];
}
-
- static deletePromises(ids: string[]): void {
- for (const id of ids) {
- delete WorkbenchRunningPromises.promises[id];
- }
- }
}
diff --git a/web-console/src/utils/druid-query.spec.ts
b/web-console/src/utils/druid-query.spec.ts
index 3fd41d6d90..ee867ff47e 100644
--- a/web-console/src/utils/druid-query.spec.ts
+++ b/web-console/src/utils/druid-query.spec.ts
@@ -21,10 +21,10 @@ import { sane } from '@druid-toolkit/query';
import { DruidError, getDruidErrorMessage } from './druid-query';
describe('DruidQuery', () => {
- describe('DruidError.parsePosition', () => {
+ describe('DruidError.extractStartRowColumn', () => {
it('works for single error 1', () => {
expect(
- DruidError.extractPosition({
+ DruidError.extractStartRowColumn({
sourceType: 'sql',
line: '2',
column: '12',
@@ -39,7 +39,7 @@ describe('DruidQuery', () => {
it('works for range', () => {
expect(
- DruidError.extractPosition({
+ DruidError.extractStartRowColumn({
sourceType: 'sql',
line: '1',
column: '16',
@@ -51,8 +51,37 @@ describe('DruidQuery', () => {
).toEqual({
row: 0,
column: 15,
- endRow: 0,
- endColumn: 16,
+ });
+ });
+ });
+
+ describe('DruidError.extractEndRowColumn', () => {
+ it('works for single error 1', () => {
+ expect(
+ DruidError.extractEndRowColumn({
+ sourceType: 'sql',
+ line: '2',
+ column: '12',
+ token: "AS \\'l\\'",
+ expected: '...',
+ }),
+ ).toBeUndefined();
+ });
+
+ it('works for range', () => {
+ expect(
+ DruidError.extractEndRowColumn({
+ sourceType: 'sql',
+ line: '1',
+ column: '16',
+ endLine: '1',
+ endColumn: '17',
+ token: "AS \\'l\\'",
+ expected: '...',
+ }),
+ ).toEqual({
+ row: 0,
+ column: 16,
});
});
});
diff --git a/web-console/src/utils/druid-query.ts
b/web-console/src/utils/druid-query.ts
index f87d18feed..6040950d11 100644
--- a/web-console/src/utils/druid-query.ts
+++ b/web-console/src/utils/druid-query.ts
@@ -22,8 +22,8 @@ import axios from 'axios';
import { Api } from '../singletons';
+import type { RowColumn } from './general';
import { assemble } from './general';
-import type { RowColumn } from './query-cursor';
const CANCELED_MESSAGE = 'Query canceled by user.';
@@ -109,20 +109,28 @@ export function getDruidErrorMessage(e: any): string {
}
export class DruidError extends Error {
- static extractPosition(context: Record<string, any> | undefined): RowColumn
| undefined {
+ static extractStartRowColumn(
+ context: Record<string, any> | undefined,
+ offsetLines = 0,
+ ): RowColumn | undefined {
if (context?.sourceType !== 'sql' || !context.line || !context.column)
return;
- const rowColumn: RowColumn = {
- row: Number(context.line) - 1,
+ return {
+ row: Number(context.line) - 1 + offsetLines,
column: Number(context.column) - 1,
};
+ }
- if (context.endLine && context.endColumn) {
- rowColumn.endRow = Number(context.endLine) - 1;
- rowColumn.endColumn = Number(context.endColumn) - 1;
- }
+ static extractEndRowColumn(
+ context: Record<string, any> | undefined,
+ offsetLines = 0,
+ ): RowColumn | undefined {
+ if (context?.sourceType !== 'sql' || !context.endLine ||
!context.endColumn) return;
- return rowColumn;
+ return {
+ row: Number(context.endLine) - 1 + offsetLines,
+ column: Number(context.endColumn) - 1,
+ };
}
static positionToIndex(str: string, line: number, column: number): number {
@@ -256,7 +264,8 @@ export class DruidError extends Error {
public errorMessage?: string;
public errorMessageWithoutExpectation?: string;
public expectation?: string;
- public position?: RowColumn;
+ public startRowColumn?: RowColumn;
+ public endRowColumn?: RowColumn;
public suggestion?: QuerySuggestion;
// Deprecated
@@ -264,7 +273,7 @@ export class DruidError extends Error {
public errorClass?: string;
public host?: string;
- constructor(e: any, skipLines = 0) {
+ constructor(e: any, offsetLines = 0) {
super(axios.isCancel(e) ? CANCELED_MESSAGE : getDruidErrorMessage(e));
if (axios.isCancel(e)) {
this.canceled = true;
@@ -286,14 +295,15 @@ export class DruidError extends Error {
Object.assign(this, druidErrorResponse);
if (this.errorMessage) {
- if (skipLines) {
+ if (offsetLines) {
this.errorMessage = this.errorMessage.replace(
/line \[(\d+)],/g,
- (_, c) => `line [${Number(c) - skipLines}],`,
+ (_, c) => `line [${Number(c) + offsetLines}],`,
);
}
- this.position = DruidError.extractPosition(this.context);
+ this.startRowColumn = DruidError.extractStartRowColumn(this.context,
offsetLines);
+ this.endRowColumn = DruidError.extractEndRowColumn(this.context,
offsetLines);
this.suggestion = DruidError.getSuggestion(this.errorMessage);
const expectationIndex = this.errorMessage.indexOf('Was expecting one
of');
diff --git a/web-console/src/utils/general.spec.ts
b/web-console/src/utils/general.spec.ts
index 7b380ae7cf..a044c0dc23 100644
--- a/web-console/src/utils/general.spec.ts
+++ b/web-console/src/utils/general.spec.ts
@@ -175,15 +175,24 @@ describe('general', () => {
describe('offsetToRowColumn', () => {
it('works', () => {
- expect(offsetToRowColumn('Hello\nThis is a test\nstring.',
-6)).toBeUndefined();
- expect(offsetToRowColumn('Hello\nThis is a test\nstring.',
666)).toBeUndefined();
- expect(offsetToRowColumn('Hello\nThis is a test\nstring.', 3)).toEqual({
+ const str = 'Hello\nThis is a test\nstring.';
+ expect(offsetToRowColumn(str, -6)).toBeUndefined();
+ expect(offsetToRowColumn(str, 666)).toBeUndefined();
+ expect(offsetToRowColumn(str, 3)).toEqual({
+ row: 0,
column: 3,
+ });
+ expect(offsetToRowColumn(str, 5)).toEqual({
row: 0,
+ column: 5,
});
- expect(offsetToRowColumn('Hello\nThis is a test\nstring.', 24)).toEqual({
+ expect(offsetToRowColumn(str, 24)).toEqual({
+ row: 2,
column: 3,
+ });
+ expect(offsetToRowColumn(str, str.length)).toEqual({
row: 2,
+ column: 7,
});
});
});
diff --git a/web-console/src/utils/general.tsx
b/web-console/src/utils/general.tsx
index a9264a9768..a871731f6b 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -156,7 +156,7 @@ function identity<T>(x: T): T {
export function lookupBy<T, Q = T>(
array: readonly T[],
- keyFn: (x: T, index: number) => string = String,
+ keyFn: (x: T, index: number) => string | number = String,
valueFn?: (x: T, index: number) => Q,
): Record<string, Q> {
if (!valueFn) valueFn = identity as any;
@@ -521,17 +521,19 @@ export function generate8HexId(): string {
return (Math.random() * 1e10).toString(16).replace('.', '').slice(0, 8);
}
-export function offsetToRowColumn(
- str: string,
- offset: number,
-): { row: number; column: number } | undefined {
+export interface RowColumn {
+ row: number;
+ column: number;
+}
+
+export function offsetToRowColumn(str: string, offset: number): RowColumn |
undefined {
// Ensure offset is within the string length
if (offset < 0 || offset > str.length) return;
const lines = str.split('\n');
for (let row = 0; row < lines.length; row++) {
const line = lines[row];
- if (offset < line.length) {
+ if (offset <= line.length) {
return {
row,
column: offset,
diff --git a/web-console/src/utils/query-cursor.ts
b/web-console/src/utils/query-cursor.ts
index 158d880b82..94bbe17938 100644
--- a/web-console/src/utils/query-cursor.ts
+++ b/web-console/src/utils/query-cursor.ts
@@ -19,6 +19,8 @@
import type { SqlBase, SqlQuery } from '@druid-toolkit/query';
import { L } from '@druid-toolkit/query';
+import type { RowColumn } from './general';
+
export const EMPTY_LITERAL = L('');
const CRAZY_STRING = '[email protected].$';
@@ -36,13 +38,6 @@ export function prettyPrintSql(b: SqlBase): string {
.toString();
}
-export interface RowColumn {
- row: number;
- column: number;
- endRow?: number;
- endColumn?: number;
-}
-
export function findEmptyLiteralPosition(query: SqlQuery): RowColumn |
undefined {
const subQueryString = query.walk(b => (b === EMPTY_LITERAL ?
L(CRAZY_STRING) : b)).toString();
@@ -54,7 +49,7 @@ export function findEmptyLiteralPosition(query: SqlQuery):
RowColumn | undefined
const row = lines.length - 1;
const lastLine = lines[row];
return {
- row,
+ row: row,
column: lastLine.length,
};
}
diff --git a/web-console/src/utils/sql.spec.ts
b/web-console/src/utils/sql.spec.ts
new file mode 100644
index 0000000000..08bd45370c
--- /dev/null
+++ b/web-console/src/utils/sql.spec.ts
@@ -0,0 +1,349 @@
+/*
+ * 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 { sane } from '@druid-toolkit/query';
+
+import { findAllSqlQueriesInText, findSqlQueryPrefix } from './sql';
+
+describe('sql', () => {
+ describe('getSqlQueryPrefix', () => {
+ it('works when whole query parses', () => {
+ expect(
+ findSqlQueryPrefix(sane`
+ SELECT *
+ FROM wikipedia
+ `),
+ ).toMatchInlineSnapshot(`
+ "SELECT *
+ FROM wikipedia"
+ `);
+ });
+
+ it('works when there are two queries', () => {
+ expect(
+ findSqlQueryPrefix(sane`
+ SELECT *
+ FROM wikipedia
+
+ SELECT *
+ FROM w2
+ `),
+ ).toMatchInlineSnapshot(`
+ "SELECT *
+ FROM wikipedia"
+ `);
+ });
+
+ it('works when there are extra closing parens', () => {
+ expect(
+ findSqlQueryPrefix(sane`
+ SELECT *
+ FROM wikipedia)) lololol
+ `),
+ ).toMatchInlineSnapshot(`
+ "SELECT *
+ FROM wikipedia"
+ `);
+ });
+ });
+
+ describe('findAllSqlQueriesInText', () => {
+ it('works with separate queries', () => {
+ const text = sane`
+ SELECT *
+ FROM wikipedia
+
+ SELECT *
+ FROM w2
+ LIMIT 5
+
+ SELECT
+ `;
+
+ const found = findAllSqlQueriesInText(text);
+
+ expect(found).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "endOffset": 23,
+ "endRowColumn": Object {
+ "column": 14,
+ "row": 1,
+ },
+ "sql": "SELECT *
+ FROM wikipedia",
+ "startOffset": 0,
+ "startRowColumn": Object {
+ "column": 0,
+ "row": 0,
+ },
+ },
+ Object {
+ "endOffset": 49,
+ "endRowColumn": Object {
+ "column": 7,
+ "row": 5,
+ },
+ "sql": "SELECT *
+ FROM w2
+ LIMIT 5",
+ "startOffset": 25,
+ "startRowColumn": Object {
+ "column": 0,
+ "row": 3,
+ },
+ },
+ ]
+ `);
+ });
+
+ it('works with simple query inside', () => {
+ const text = sane`
+ SELECT
+ "channel",
+ COUNT(*) AS "Count"
+ FROM (SELECT * FROM "wikipedia")
+ GROUP BY 1
+ ORDER BY 2 DESC
+ `;
+
+ const found = findAllSqlQueriesInText(text);
+
+ expect(found).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "endOffset": 101,
+ "endRowColumn": Object {
+ "column": 15,
+ "row": 5,
+ },
+ "sql": "SELECT
+ \\"channel\\",
+ COUNT(*) AS \\"Count\\"
+ FROM (SELECT * FROM \\"wikipedia\\")
+ GROUP BY 1
+ ORDER BY 2 DESC",
+ "startOffset": 0,
+ "startRowColumn": Object {
+ "column": 0,
+ "row": 0,
+ },
+ },
+ Object {
+ "endOffset": 73,
+ "endRowColumn": Object {
+ "column": 31,
+ "row": 3,
+ },
+ "sql": "SELECT * FROM \\"wikipedia\\"",
+ "startOffset": 48,
+ "startRowColumn": Object {
+ "column": 6,
+ "row": 3,
+ },
+ },
+ ]
+ `);
+ });
+
+ it('works with CTE query', () => {
+ const text = sane`
+ WITH w1 AS (
+ SELECT channel, page FROM "wikipedia"
+ )
+ SELECT
+ page,
+ COUNT(*) AS "cnt"
+ FROM w1
+ GROUP BY 1
+ ORDER BY 2 DESC
+ `;
+
+ const found = findAllSqlQueriesInText(text);
+
+ expect(found).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "endOffset": 124,
+ "endRowColumn": Object {
+ "column": 15,
+ "row": 8,
+ },
+ "sql": "WITH w1 AS (
+ SELECT channel, page FROM \\"wikipedia\\"
+ )
+ SELECT
+ page,
+ COUNT(*) AS \\"cnt\\"
+ FROM w1
+ GROUP BY 1
+ ORDER BY 2 DESC",
+ "startOffset": 0,
+ "startRowColumn": Object {
+ "column": 0,
+ "row": 0,
+ },
+ },
+ Object {
+ "endOffset": 52,
+ "endRowColumn": Object {
+ "column": 39,
+ "row": 1,
+ },
+ "sql": "SELECT channel, page FROM \\"wikipedia\\"",
+ "startOffset": 15,
+ "startRowColumn": Object {
+ "column": 2,
+ "row": 1,
+ },
+ },
+ Object {
+ "endOffset": 124,
+ "endRowColumn": Object {
+ "column": 15,
+ "row": 8,
+ },
+ "sql": "SELECT
+ page,
+ COUNT(*) AS \\"cnt\\"
+ FROM w1
+ GROUP BY 1
+ ORDER BY 2 DESC",
+ "startOffset": 55,
+ "startRowColumn": Object {
+ "column": 0,
+ "row": 3,
+ },
+ },
+ ]
+ `);
+ });
+
+ it('works with replace query', () => {
+ const text = sane`
+ REPLACE INTO "wikipedia" OVERWRITE ALL
+ WITH "ext" AS (SELECT *
+ FROM TABLE(
+ EXTERN(
+
'{"type":"http","uris":["https://druid.apache.org/data/wikipedia.json.gz"]}',
+ '{"type":"json"}'
+ )
+ ) EXTEND ("isRobot" VARCHAR, "channel" VARCHAR, "timestamp" VARCHAR))
+ SELECT
+ TIME_PARSE("timestamp") AS "__time",
+ "isRobot",
+ "channel"
+ FROM "ext"
+ PARTITIONED BY DAY
+ `;
+
+ const found = findAllSqlQueriesInText(text);
+
+ expect(found).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "endOffset": 363,
+ "endRowColumn": Object {
+ "column": 18,
+ "row": 13,
+ },
+ "sql": "REPLACE INTO \\"wikipedia\\" OVERWRITE ALL
+ WITH \\"ext\\" AS (SELECT *
+ FROM TABLE(
+ EXTERN(
+
'{\\"type\\":\\"http\\",\\"uris\\":[\\"https://druid.apache.org/data/wikipedia.json.gz\\"]}',
+ '{\\"type\\":\\"json\\"}'
+ )
+ ) EXTEND (\\"isRobot\\" VARCHAR, \\"channel\\" VARCHAR,
\\"timestamp\\" VARCHAR))
+ SELECT
+ TIME_PARSE(\\"timestamp\\") AS \\"__time\\",
+ \\"isRobot\\",
+ \\"channel\\"
+ FROM \\"ext\\"
+ PARTITIONED BY DAY",
+ "startOffset": 0,
+ "startRowColumn": Object {
+ "column": 0,
+ "row": 0,
+ },
+ },
+ Object {
+ "endOffset": 344,
+ "endRowColumn": Object {
+ "column": 10,
+ "row": 12,
+ },
+ "sql": "WITH \\"ext\\" AS (SELECT *
+ FROM TABLE(
+ EXTERN(
+
'{\\"type\\":\\"http\\",\\"uris\\":[\\"https://druid.apache.org/data/wikipedia.json.gz\\"]}',
+ '{\\"type\\":\\"json\\"}'
+ )
+ ) EXTEND (\\"isRobot\\" VARCHAR, \\"channel\\" VARCHAR,
\\"timestamp\\" VARCHAR))
+ SELECT
+ TIME_PARSE(\\"timestamp\\") AS \\"__time\\",
+ \\"isRobot\\",
+ \\"channel\\"
+ FROM \\"ext\\"",
+ "startOffset": 39,
+ "startRowColumn": Object {
+ "column": 0,
+ "row": 1,
+ },
+ },
+ Object {
+ "endOffset": 261,
+ "endRowColumn": Object {
+ "column": 68,
+ "row": 7,
+ },
+ "sql": "SELECT *
+ FROM TABLE(
+ EXTERN(
+
'{\\"type\\":\\"http\\",\\"uris\\":[\\"https://druid.apache.org/data/wikipedia.json.gz\\"]}',
+ '{\\"type\\":\\"json\\"}'
+ )
+ ) EXTEND (\\"isRobot\\" VARCHAR, \\"channel\\" VARCHAR,
\\"timestamp\\" VARCHAR)",
+ "startOffset": 54,
+ "startRowColumn": Object {
+ "column": 15,
+ "row": 1,
+ },
+ },
+ Object {
+ "endOffset": 344,
+ "endRowColumn": Object {
+ "column": 10,
+ "row": 12,
+ },
+ "sql": "SELECT
+ TIME_PARSE(\\"timestamp\\") AS \\"__time\\",
+ \\"isRobot\\",
+ \\"channel\\"
+ FROM \\"ext\\"",
+ "startOffset": 263,
+ "startRowColumn": Object {
+ "column": 0,
+ "row": 8,
+ },
+ },
+ ]
+ `);
+ });
+ });
+});
diff --git a/web-console/src/utils/sql.ts b/web-console/src/utils/sql.ts
index 18019561ac..7404ee3137 100644
--- a/web-console/src/utils/sql.ts
+++ b/web-console/src/utils/sql.ts
@@ -16,7 +16,17 @@
* limitations under the License.
*/
-import { SqlColumn, SqlExpression, SqlFunction, SqlLiteral, SqlStar } from
'@druid-toolkit/query';
+import {
+ SqlColumn,
+ SqlExpression,
+ SqlFunction,
+ SqlLiteral,
+ SqlQuery,
+ SqlStar,
+} from '@druid-toolkit/query';
+
+import type { RowColumn } from './general';
+import { offsetToRowColumn } from './general';
export function timeFormatToSql(timeFormat: string): SqlExpression | undefined
{
switch (timeFormat) {
@@ -60,3 +70,72 @@ export function convertToGroupByExpression(ex:
SqlExpression): SqlExpression | u
return newEx.as((ex.getOutputName() || 'grouped').replace(/^[a-z]+_/i, ''));
}
+
+function extractQueryPrefix(text: string): string {
+ let q = SqlQuery.parse(text);
+
+ // The parser will parse a SELECT query with a partitionedByClause and
clusteredByClause but that is not valid, remove them from the query
+ if (!q.getIngestTable() && (q.partitionedByClause || q.clusteredByClause)) {
+ q =
q.changePartitionedByClause(undefined).changeClusteredByClause(undefined);
+ }
+
+ return q.toString().trimEnd();
+}
+
+export function findSqlQueryPrefix(text: string): string | undefined {
+ try {
+ return extractQueryPrefix(text);
+ } catch (e) {
+ const startOffset = e.location?.start?.offset;
+ if (typeof startOffset !== 'number') return;
+ const prefix = text.slice(0, startOffset);
+ // Try to trim to where the error came from
+ try {
+ return extractQueryPrefix(prefix);
+ } catch {
+ // Try to trim out last word
+ try {
+ return extractQueryPrefix(prefix.replace(/\s*\w+$/, ''));
+ } catch {
+ return;
+ }
+ }
+ }
+}
+
+export interface QuerySlice {
+ startOffset: number;
+ startRowColumn: RowColumn;
+ endOffset: number;
+ endRowColumn: RowColumn;
+ sql: string;
+}
+
+export function findAllSqlQueriesInText(text: string): QuerySlice[] {
+ const found: QuerySlice[] = [];
+
+ let remainingText = text;
+ let offset = 0;
+ let m: RegExpExecArray | null = null;
+ do {
+ m = /SELECT|WITH|INSERT|REPLACE/i.exec(remainingText);
+ if (m) {
+ const sql = findSqlQueryPrefix(remainingText.slice(m.index));
+ const advanceBy = m.index + m[0].length; // Skip the initial word
+ if (sql) {
+ const endIndex = m.index + sql.length;
+ found.push({
+ startOffset: offset + m.index,
+ startRowColumn: offsetToRowColumn(text, offset + m.index)!,
+ endOffset: offset + endIndex,
+ endRowColumn: offsetToRowColumn(text, offset + endIndex)!,
+ sql,
+ });
+ }
+ remainingText = remainingText.slice(advanceBy);
+ offset += advanceBy;
+ }
+ } while (m);
+
+ return found;
+}
diff --git
a/web-console/src/views/sql-data-loader-view/column-editor/column-editor.tsx
b/web-console/src/views/sql-data-loader-view/column-editor/column-editor.tsx
index 53ca8b6863..fcae24eb79 100644
--- a/web-console/src/views/sql-data-loader-view/column-editor/column-editor.tsx
+++ b/web-console/src/views/sql-data-loader-view/column-editor/column-editor.tsx
@@ -164,7 +164,6 @@ export const ColumnEditor = React.memo(function
ColumnEditor(props: ColumnEditor
</FormGroup>
<FormGroup label="SQL expression">
<FlexibleQueryInput
- autoHeight={false}
showGutter={false}
placeholder="expression"
queryString={effectiveExpressionString}
diff --git
a/web-console/src/views/sql-data-loader-view/expression-editor-dialog/expression-editor-dialog.tsx
b/web-console/src/views/sql-data-loader-view/expression-editor-dialog/expression-editor-dialog.tsx
index c762a6f645..1b6d08df20 100644
---
a/web-console/src/views/sql-data-loader-view/expression-editor-dialog/expression-editor-dialog.tsx
+++
b/web-console/src/views/sql-data-loader-view/expression-editor-dialog/expression-editor-dialog.tsx
@@ -57,7 +57,6 @@ export const ExpressionEditorDialog = React.memo(function
ExpressionEditorDialog
<div className={Classes.DIALOG_BODY}>
<FormGroup>
<FlexibleQueryInput
- autoHeight={false}
showGutter={false}
placeholder="expression"
queryString={formula}
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 fd3503d519..8b5ee9f82f 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
@@ -58,7 +58,7 @@ import {
ingestQueryPatternToQuery,
possibleDruidFormatForValues,
TIME_COLUMN,
- WorkbenchQueryPart,
+ WorkbenchQuery,
} from '../../../druid-models';
import {
executionBackgroundResultStatusCheck,
@@ -483,8 +483,7 @@ export const SchemaStep = function SchemaStep(props:
SchemaStepProps) {
const [previewResultState] = useQueryManager<string, QueryResult,
Execution>({
query: previewQueryString,
processQuery: async (previewQueryString, cancelToken) => {
- const taskEngine =
WorkbenchQueryPart.isTaskEngineNeeded(previewQueryString);
- if (taskEngine) {
+ if (WorkbenchQuery.isTaskEngineNeeded(previewQueryString)) {
return extractResult(
await submitTaskQuery({
query: previewQueryString,
@@ -872,7 +871,6 @@ export const SchemaStep = function SchemaStep(props:
SchemaStepProps) {
))}
{effectiveMode === 'sql' && (
<FlexibleQueryInput
- autoHeight={false}
queryString={queryString}
onQueryStringChange={onQueryStringChange}
columnMetadata={undefined}
diff --git a/web-console/src/views/workbench-view/demo-queries.ts
b/web-console/src/views/workbench-view/demo-queries.ts
index d5ee1fe6cd..c301be16ad 100644
--- a/web-console/src/views/workbench-view/demo-queries.ts
+++ b/web-console/src/views/workbench-view/demo-queries.ts
@@ -23,7 +23,7 @@ const BASE_QUERY = WorkbenchQuery.blank();
export function getDemoQueries(): TabEntry[] {
function makeDemoQuery(queryString: string): WorkbenchQuery {
- return BASE_QUERY.duplicate().changeQueryString(queryString.trim());
+ return BASE_QUERY.changeQueryString(queryString.trim());
}
return [
diff --git
a/web-console/src/views/workbench-view/execution-details-pane/execution-details-pane.tsx
b/web-console/src/views/workbench-view/execution-details-pane/execution-details-pane.tsx
index 91914e1f3b..e448fd8a50 100644
---
a/web-console/src/views/workbench-view/execution-details-pane/execution-details-pane.tsx
+++
b/web-console/src/views/workbench-view/execution-details-pane/execution-details-pane.tsx
@@ -95,7 +95,6 @@ export const ExecutionDetailsPane = React.memo(function
ExecutionDetailsPane(
? String(execution.sqlQuery)
: JSONBig.stringify(execution.nativeQuery, undefined, 2)
}
- autoHeight={false}
/>
);
diff --git
a/web-console/src/views/workbench-view/flexible-query-input/__snapshots__/flexible-query-input.spec.tsx.snap
b/web-console/src/views/workbench-view/flexible-query-input/__snapshots__/flexible-query-input.spec.tsx.snap
index 8a36beabda..a21c1eaf5d 100644
---
a/web-console/src/views/workbench-view/flexible-query-input/__snapshots__/flexible-query-input.spec.tsx.snap
+++
b/web-console/src/views/workbench-view/flexible-query-input/__snapshots__/flexible-query-input.spec.tsx.snap
@@ -12,7 +12,7 @@ exports[`FlexibleQueryInput matches snapshot 1`] = `
class="flexible-query-input"
>
<div
- class="ace-container"
+ class="ace-container query-idle"
>
<div
class=" ace_editor ace_hidpi ace-tm placeholder-padding no-background
ace_focus"
diff --git
a/web-console/src/views/workbench-view/flexible-query-input/flexible-query-input.scss
b/web-console/src/views/workbench-view/flexible-query-input/flexible-query-input.scss
index ab4bc1b5c9..f3604e85c0 100644
---
a/web-console/src/views/workbench-view/flexible-query-input/flexible-query-input.scss
+++
b/web-console/src/views/workbench-view/flexible-query-input/flexible-query-input.scss
@@ -16,6 +16,8 @@
* limitations under the License.
*/
+@import '../../../variables';
+
.flexible-query-input {
position: relative;
@@ -24,4 +26,54 @@
width: 100%;
height: 100%;
}
+
+ .sub-query-highlight {
+ position: absolute;
+ background: $gray1;
+ }
+
+ .sub-query-gutter-marker {
+ cursor: pointer;
+
+ &:before {
+ content: '⏵';
+ position: absolute;
+ top: 3px;
+ left: 2px;
+ width: 12px;
+ height: 12px;
+ background: $blue3;
+ color: white;
+ line-height: 12px;
+ text-align: center;
+ border-radius: 2px;
+ }
+
+ &:hover:before {
+ background: $blue2;
+ }
+
+ &:hover:after {
+ content: 'Run';
+ position: absolute;
+ top: 0;
+ left: 16px;
+ right: 0;
+ background: #383d57;
+ text-align: left;
+ animation: sharpFadeIn 1s;
+ }
+ }
+}
+
+@keyframes sharpFadeIn {
+ 0% {
+ opacity: 0;
+ }
+ 90% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
}
diff --git
a/web-console/src/views/workbench-view/flexible-query-input/flexible-query-input.spec.tsx
b/web-console/src/views/workbench-view/flexible-query-input/flexible-query-input.spec.tsx
index 0690447686..9a2b5b2fb5 100644
---
a/web-console/src/views/workbench-view/flexible-query-input/flexible-query-input.spec.tsx
+++
b/web-console/src/views/workbench-view/flexible-query-input/flexible-query-input.spec.tsx
@@ -24,11 +24,7 @@ import { FlexibleQueryInput } from './flexible-query-input';
describe('FlexibleQueryInput', () => {
it('matches snapshot', () => {
const sqlControl = (
- <FlexibleQueryInput
- queryString="hello world"
- autoHeight={false}
- onQueryStringChange={() => {}}
- />
+ <FlexibleQueryInput queryString="hello world" onQueryStringChange={() =>
{}} />
);
const { container } = render(sqlControl);
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 5f1141aaf2..e9eb368829 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
@@ -16,11 +16,14 @@
* limitations under the License.
*/
+import { Intent } from '@blueprintjs/core';
+import { IconNames } from '@blueprintjs/icons';
import { ResizeSensor2 } from '@blueprintjs/popover2';
-import { C, T } from '@druid-toolkit/query';
+import { C, dedupe, T } from '@druid-toolkit/query';
import type { Ace } from 'ace-builds';
import ace from 'ace-builds';
import classNames from 'classnames';
+import debounce from 'lodash.debounce';
import escape from 'lodash.escape';
import React from 'react';
import AceEditor from 'react-ace';
@@ -32,16 +35,16 @@ import {
SQL_KEYWORDS,
} from '../../../../lib/keywords';
import { SQL_DATA_TYPES, SQL_FUNCTIONS } from '../../../../lib/sql-docs';
+import { AppToaster } from '../../../singletons';
import { AceEditorStateCache } from
'../../../singletons/ace-editor-state-cache';
-import type { ColumnMetadata, RowColumn } from '../../../utils';
-import { uniq } from '../../../utils';
+import type { ColumnMetadata, QuerySlice, RowColumn } from '../../../utils';
+import { findAllSqlQueriesInText, findMap, uniq } from '../../../utils';
import './flexible-query-input.scss';
const langTools = ace.require('ace/ext/language_tools');
const V_PADDING = 10;
-const SCROLLBAR = 20;
const COMPLETER = {
insertMatch: (editor: any, data: Ace.Completion) => {
@@ -58,8 +61,8 @@ interface ItemDescription {
export interface FlexibleQueryInputProps {
queryString: string;
onQueryStringChange?: (newQueryString: string) => void;
- autoHeight: boolean;
- minRows?: number;
+ runQuerySlice?: (querySlice: QuerySlice) => void;
+ running?: boolean;
showGutter?: boolean;
placeholder?: string;
columnMetadata?: readonly ColumnMetadata[];
@@ -85,6 +88,8 @@ export class FlexibleQueryInput extends React.PureComponent<
FlexibleQueryInputState
> {
private aceEditor: Ace.Editor | undefined;
+ private lastFoundQueries: QuerySlice[] = [];
+ private highlightFoundQuery: { row: number; marker: number } | undefined;
static replaceDefaultAutoCompleter(): void {
if (!langTools) return;
@@ -260,6 +265,14 @@ export class FlexibleQueryInput extends
React.PureComponent<
},
});
}
+
+ this.markQueries();
+ }
+
+ componentDidUpdate(prevProps: Readonly<FlexibleQueryInputProps>) {
+ if (this.props.queryString !== prevProps.queryString) {
+ this.markQueriesDebounced();
+ }
}
componentWillUnmount() {
@@ -267,8 +280,42 @@ export class FlexibleQueryInput extends
React.PureComponent<
if (editorStateId && this.aceEditor) {
AceEditorStateCache.saveState(editorStateId, this.aceEditor);
}
+ delete this.aceEditor;
+ }
+
+ private findAllQueriesByLine() {
+ const { queryString } = this.props;
+ const found = dedupe(findAllSqlQueriesInText(queryString), ({
startRowColumn }) =>
+ String(startRowColumn.row),
+ );
+ if (found.length <= 1) return []; // Do not highlight a single query or no
queries
+
+ // Do not report the first query if it is basically the main query minus
whitespace
+ const firstQuery = found[0].sql;
+ if (firstQuery === queryString.trim()) return found.slice(1);
+
+ return found;
}
+ private readonly markQueries = () => {
+ if (!this.props.runQuerySlice) return;
+ const { aceEditor } = this;
+ if (!aceEditor) return;
+ const session = aceEditor.getSession();
+ this.lastFoundQueries = this.findAllQueriesByLine();
+
+ session.clearBreakpoints();
+ this.lastFoundQueries.forEach(({ startRowColumn }) => {
+ // session.addGutterDecoration(startRowColumn.row,
`sub-query-gutter-marker query-${i}`);
+ session.setBreakpoint(
+ startRowColumn.row,
+ `sub-query-gutter-marker query-${startRowColumn.row}`,
+ );
+ });
+ };
+
+ private readonly markQueriesDebounced = debounce(this.markQueries, 900, {
trailing: true });
+
private readonly handleAceContainerResize = (entries: ResizeObserverEntry[])
=> {
if (entries.length !== 1) return;
this.setState({ editorHeight: entries[0].contentRect.height });
@@ -285,35 +332,16 @@ export class FlexibleQueryInput extends
React.PureComponent<
if (!aceEditor) return;
aceEditor.focus(); // Grab the focus
aceEditor.getSelection().moveCursorTo(rowColumn.row, rowColumn.column);
- if (rowColumn.endRow && rowColumn.endColumn) {
- aceEditor
- .getSelection()
- .selectToPosition({ row: rowColumn.endRow, column: rowColumn.endColumn
});
- }
+ // If we had an end we could also do
+ // aceEditor.getSelection().selectToPosition({ row: endRow, column:
endColumn });
}
renderAce() {
- const {
- queryString,
- onQueryStringChange,
- autoHeight,
- minRows,
- showGutter,
- placeholder,
- editorStateId,
- } = this.props;
+ const { queryString, onQueryStringChange, showGutter, placeholder,
editorStateId } = this.props;
const { editorHeight } = this.state;
const jsonMode = queryString.trim().startsWith('{');
- let height: number;
- if (autoHeight) {
- height =
- Math.max(queryString.split('\n').length, minRows ?? 2) * 18 + 2 *
V_PADDING + SCROLLBAR;
- } else {
- height = editorHeight;
- }
-
return (
<AceEditor
mode={jsonMode ? 'hjson' : 'dsql'}
@@ -327,7 +355,7 @@ export class FlexibleQueryInput extends React.PureComponent<
focus
fontSize={13}
width="100%"
- height={height + 'px'}
+ height={editorHeight + 'px'}
showGutter={showGutter}
showPrintMargin={false}
value={queryString}
@@ -359,18 +387,83 @@ export class FlexibleQueryInput extends
React.PureComponent<
}
render() {
- const { autoHeight } = this.props;
+ const { runQuerySlice, running } = this.props;
// Set the key in the AceEditor to force a rebind and prevent an error
that happens otherwise
return (
<div className="flexible-query-input">
- {autoHeight ? (
- this.renderAce()
- ) : (
- <ResizeSensor2 onResize={this.handleAceContainerResize}>
- <div className="ace-container">{this.renderAce()}</div>
- </ResizeSensor2>
- )}
+ <ResizeSensor2 onResize={this.handleAceContainerResize}>
+ <div
+ className={classNames('ace-container', running ? 'query-running' :
'query-idle')}
+ onClick={e => {
+ if (!runQuerySlice) return;
+ const classes = [...(e.target as any).classList];
+ if (!classes.includes('sub-query-gutter-marker')) return;
+ const row = findMap(classes, c => {
+ const m = /^query-(\d+)$/.exec(c);
+ return m ? Number(m[1]) : undefined;
+ });
+ if (typeof row === 'undefined') return;
+
+ // Gutter query marker clicked on line ${row}
+ const slice = this.lastFoundQueries.find(
+ ({ startRowColumn }) => startRowColumn.row === row,
+ );
+ if (!slice) return;
+
+ if (running) {
+ AppToaster.show({
+ icon: IconNames.WARNING_SIGN,
+ intent: Intent.WARNING,
+ message: `Another query is currently running`,
+ });
+ return;
+ }
+
+ runQuerySlice(slice);
+ }}
+ onMouseOver={e => {
+ if (!runQuerySlice) return;
+ const aceEditor = this.aceEditor;
+ if (!aceEditor) return;
+
+ const classes = [...(e.target as any).classList];
+ if (!classes.includes('sub-query-gutter-marker')) return;
+ const row = findMap(classes, c => {
+ const m = /^query-(\d+)$/.exec(c);
+ return m ? Number(m[1]) : undefined;
+ });
+ if (typeof row === 'undefined' || this.highlightFoundQuery?.row
=== row) return;
+
+ const slice = this.lastFoundQueries.find(
+ ({ startRowColumn }) => startRowColumn.row === row,
+ );
+ if (!slice) return;
+ const marker = aceEditor
+ .getSession()
+ .addMarker(
+ new ace.Range(
+ slice.startRowColumn.row,
+ slice.startRowColumn.column,
+ slice.endRowColumn.row,
+ slice.endRowColumn.column,
+ ),
+ 'sub-query-highlight',
+ 'text',
+ );
+ this.highlightFoundQuery = { row, marker };
+ }}
+ onMouseOut={() => {
+ if (!this.highlightFoundQuery) return;
+ const aceEditor = this.aceEditor;
+ if (!aceEditor) return;
+
aceEditor.getSession().removeMarker(this.highlightFoundQuery.marker);
+ this.highlightFoundQuery = undefined;
+ }}
+ >
+ {this.renderAce()}
+ </div>
+ </ResizeSensor2>
</div>
);
}
diff --git
a/web-console/src/views/workbench-view/helper-query/helper-query.scss
b/web-console/src/views/workbench-view/helper-query/helper-query.scss
deleted file mode 100644
index 4233353c71..0000000000
--- a/web-console/src/views/workbench-view/helper-query/helper-query.scss
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * 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 '../../../variables';
-
-.helper-query {
- position: relative;
- @include card-like;
-
- .query-top-bar {
- position: relative;
- height: 36px;
- display: flex;
- align-items: center;
- gap: 4px;
- white-space: nowrap;
-
- .corner {
- position: absolute;
- top: 50%;
- right: 3px;
- transform: translate(0, -50%);
- @include card-background;
- }
- }
-
- .flexible-query-input {
- border-top: 1px solid rgba($dark-gray1, 0.5);
- border-bottom: 1px solid rgba($dark-gray1, 0.5);
- }
-
- .query-control-bar {
- position: relative;
- width: 100%;
- height: 30px;
- display: flex;
- gap: 10px;
- align-items: center;
-
- .execution-timer-panel,
- .execution-summary-panel {
- position: absolute;
- top: 0;
- right: 0;
- }
- }
-
- .init-pane {
- text-align: center;
- flex: 1;
- border-top: 1px solid rgba($dark-gray1, 0.5);
-
- p {
- position: relative;
- top: 38%;
- font-size: 15px;
- }
- }
-
- .output-pane {
- overflow: hidden;
- position: relative;
- height: 254px;
- border-top: 1px solid rgba($dark-gray1, 0.5);
-
- > * {
- position: absolute;
- width: 100%;
- height: 100%;
- }
-
- .error-container {
- position: relative;
-
- .execution-error-pane {
- position: absolute;
- top: 5px;
- left: 5px;
- right: 5px;
- height: 150px;
- width: auto;
- }
- }
- }
-}
diff --git a/web-console/src/views/workbench-view/helper-query/helper-query.tsx
b/web-console/src/views/workbench-view/helper-query/helper-query.tsx
deleted file mode 100644
index 1b0f7a629e..0000000000
--- a/web-console/src/views/workbench-view/helper-query/helper-query.tsx
+++ /dev/null
@@ -1,473 +0,0 @@
-/*
- * 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 { Button, ButtonGroup, InputGroup, Intent, Menu, MenuItem } from
'@blueprintjs/core';
-import { IconNames } from '@blueprintjs/icons';
-import { Popover2 } from '@blueprintjs/popover2';
-import type { QueryResult } from '@druid-toolkit/query';
-import { QueryRunner, SqlQuery } from '@druid-toolkit/query';
-import axios from 'axios';
-import type { JSX } from 'react';
-import React, { useCallback, useEffect, useRef, useState } from 'react';
-import { useStore } from 'zustand';
-
-import { Loader, QueryErrorPane } from '../../../components';
-import type { DruidEngine, LastExecution, QueryContext } from
'../../../druid-models';
-import {
- Execution,
- fitExternalConfigPattern,
- summarizeExternalConfig,
- WorkbenchQuery,
-} from '../../../druid-models';
-import {
- executionBackgroundStatusCheck,
- maybeGetClusterCapacity,
- reattachTaskExecution,
- submitTaskQuery,
-} from '../../../helpers';
-import { usePermanentCallback, useQueryManager } from '../../../hooks';
-import { Api, AppToaster } from '../../../singletons';
-import { ExecutionStateCache } from
'../../../singletons/execution-state-cache';
-import { WorkbenchHistory } from '../../../singletons/workbench-history';
-import type { WorkbenchRunningPromise } from
'../../../singletons/workbench-running-promises';
-import { WorkbenchRunningPromises } from
'../../../singletons/workbench-running-promises';
-import type { ColumnMetadata, QueryAction, RowColumn } from '../../../utils';
-import { DruidError, QueryManager } from '../../../utils';
-import { CapacityAlert } from '../capacity-alert/capacity-alert';
-import type { ExecutionDetailsTab } from
'../execution-details-pane/execution-details-pane';
-import { ExecutionErrorPane } from
'../execution-error-pane/execution-error-pane';
-import { ExecutionProgressPane } from
'../execution-progress-pane/execution-progress-pane';
-import { ExecutionStagesPane } from
'../execution-stages-pane/execution-stages-pane';
-import { ExecutionSummaryPanel } from
'../execution-summary-panel/execution-summary-panel';
-import { ExecutionTimerPanel } from
'../execution-timer-panel/execution-timer-panel';
-import { FlexibleQueryInput } from
'../flexible-query-input/flexible-query-input';
-import { IngestSuccessPane } from '../ingest-success-pane/ingest-success-pane';
-import { metadataStateStore } from '../metadata-state-store';
-import { ResultTablePane } from '../result-table-pane/result-table-pane';
-import { RunPanel } from '../run-panel/run-panel';
-import { workStateStore } from '../work-state-store';
-
-import './helper-query.scss';
-
-const queryRunner = new QueryRunner({
- inflateDateStrategy: 'none',
-});
-
-export interface HelperQueryProps {
- query: WorkbenchQuery;
- mandatoryQueryContext: QueryContext | undefined;
- columnMetadata: readonly ColumnMetadata[] | undefined;
- onQueryChange(newQuery: WorkbenchQuery): void;
- onQueryTab(newQuery: WorkbenchQuery, tabName?: string): void;
- onDelete(): void;
- onDetails(id: string, initTab?: ExecutionDetailsTab): void;
- queryEngines: DruidEngine[];
- clusterCapacity: number | undefined;
- goToTask(taskId: string): void;
-}
-
-export const HelperQuery = React.memo(function HelperQuery(props:
HelperQueryProps) {
- const {
- query,
- columnMetadata,
- mandatoryQueryContext,
- onQueryChange,
- onQueryTab,
- onDelete,
- onDetails,
- queryEngines,
- clusterCapacity,
- goToTask,
- } = props;
- const [alertElement, setAlertElement] = useState<JSX.Element | undefined>();
-
- // Store the cancellation function for natively run queries allowing us to
trigger it only when the user explicitly clicks "cancel" (vs changing tab)
- const nativeQueryCancelFnRef = useRef<() => void>();
-
- const handleQueryStringChange = usePermanentCallback((queryString: string)
=> {
- onQueryChange(query.changeQueryString(queryString));
- });
-
- const parsedQuery = query.getParsedQuery();
- const handleQueryAction = usePermanentCallback((queryAction: QueryAction) =>
{
- if (!(parsedQuery instanceof SqlQuery)) return;
-
onQueryChange(query.changeQueryString(parsedQuery.apply(queryAction).toString()));
-
- if (shouldAutoRun()) {
- setTimeout(() => void handleRun(false), 20);
- }
- });
-
- function shouldAutoRun(): boolean {
- if (query.getEffectiveEngine() !== 'sql-native') return false;
- const queryDuration = executionState.data?.result?.queryDuration;
- return Boolean(queryDuration && queryDuration < 10000);
- }
-
- const queryInputRef = useRef<FlexibleQueryInput | null>(null);
-
- const id = query.getId();
- const [executionState, queryManager] = useQueryManager<
- WorkbenchQuery | WorkbenchRunningPromise | LastExecution,
- Execution,
- Execution,
- DruidError
- >({
- initQuery: ExecutionStateCache.getState(id)
- ? undefined
- : WorkbenchRunningPromises.getPromise(id) || query.getLastExecution(),
- initState: ExecutionStateCache.getState(id),
- processQuery: async (q, cancelToken) => {
- if (q instanceof WorkbenchQuery) {
- ExecutionStateCache.deleteState(id);
-
- const { engine, query, sqlPrefixLines, cancelQueryId } =
q.getApiQuery();
-
- switch (engine) {
- case 'sql-msq-task':
- return await submitTaskQuery({
- query,
- prefixLines: sqlPrefixLines,
- cancelToken,
- preserveOnTermination: true,
- onSubmitted: id => {
- onQueryChange(props.query.changeLastExecution({ engine, id }));
- },
- });
-
- case 'native':
- case 'sql-native': {
- if (cancelQueryId) {
- void cancelToken.promise
- .then(cancel => {
- if (cancel.message === QueryManager.TERMINATION_MESSAGE)
return;
- return Api.instance.delete(
- `/druid/v2${engine === 'sql-native' ? '/sql' :
''}/${Api.encodePath(
- cancelQueryId,
- )}`,
- );
- })
- .catch(() => {});
- }
-
- onQueryChange(props.query.changeLastExecution(undefined));
-
- let result: QueryResult;
- try {
- const resultPromise = queryRunner.runQuery({
- query,
- extraQueryContext: mandatoryQueryContext,
- cancelToken: new axios.CancelToken(cancelFn => {
- nativeQueryCancelFnRef.current = cancelFn;
- }),
- });
- WorkbenchRunningPromises.storePromise(id, { promise:
resultPromise, sqlPrefixLines });
-
- result = await resultPromise;
- nativeQueryCancelFnRef.current = undefined;
- } catch (e) {
- nativeQueryCancelFnRef.current = undefined;
- throw new DruidError(e, sqlPrefixLines);
- }
-
- return Execution.fromResult(engine, result);
- }
- }
- } else if (WorkbenchRunningPromises.isWorkbenchRunningPromise(q)) {
- let result: QueryResult;
- try {
- result = await q.promise;
- } catch (e) {
- WorkbenchRunningPromises.deletePromise(id);
- throw new DruidError(e, q.sqlPrefixLines);
- }
-
- WorkbenchRunningPromises.deletePromise(id);
- return Execution.fromResult('sql-native', result);
- } else {
- switch (q.engine) {
- case 'sql-msq-task':
- return await reattachTaskExecution({
- id: q.id,
- cancelToken,
- preserveOnTermination: true,
- });
-
- default:
- throw new Error(`Can not reattach on ${q.engine}`);
- }
- }
- },
- backgroundStatusCheck: executionBackgroundStatusCheck,
- swallowBackgroundError: Api.isNetworkError,
- });
-
- useEffect(() => {
- if (!executionState.data) return;
- ExecutionStateCache.storeState(id, executionState);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [executionState.data, executionState.error]);
-
- const incrementWorkVersion = useStore(
- workStateStore,
- useCallback(state => state.increment, []),
- );
- useEffect(() => {
- incrementWorkVersion();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [executionState.loading, Boolean(executionState.intermediate)]);
-
- const execution = executionState.data;
-
- const incrementMetadataVersion = useStore(
- metadataStateStore,
- useCallback(state => state.increment, []),
- );
- useEffect(() => {
- if (execution?.isSuccessfulInsert()) {
- incrementMetadataVersion();
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [Boolean(execution?.isSuccessfulInsert())]);
-
- function moveToPosition(position: RowColumn) {
- const currentQueryInput = queryInputRef.current;
- if (!currentQueryInput) return;
- currentQueryInput.goToPosition(position);
- }
-
- const handleRun = usePermanentCallback(async (preview: boolean) => {
- const queryIssue = query.getIssue();
- if (queryIssue) {
- const position = WorkbenchQuery.getRowColumnFromIssue(queryIssue);
-
- AppToaster.show({
- icon: IconNames.ERROR,
- intent: Intent.DANGER,
- timeout: 90000,
- message: queryIssue,
- action: position
- ? {
- text: 'Go to issue',
- onClick: () => moveToPosition(position),
- }
- : undefined,
- });
- return;
- }
-
- if (query.getEffectiveEngine() !== 'sql-msq-task') {
- WorkbenchHistory.addQueryToHistory(query);
- queryManager.runQuery(query);
- return;
- }
-
- const effectiveQuery = preview
- ? query.makePreview()
- : query.setMaxNumTasksIfUnset(clusterCapacity);
-
- const capacityInfo = await maybeGetClusterCapacity();
-
- const effectiveMaxNumTasks = effectiveQuery.queryContext.maxNumTasks ?? 2;
- if (capacityInfo && capacityInfo.availableTaskSlots <
effectiveMaxNumTasks) {
- setAlertElement(
- <CapacityAlert
- maxNumTasks={effectiveMaxNumTasks}
- capacityInfo={capacityInfo}
- onRun={() => {
- queryManager.runQuery(effectiveQuery);
- }}
- onClose={() => {
- setAlertElement(undefined);
- }}
- />,
- );
- } else {
- queryManager.runQuery(effectiveQuery);
- }
- });
-
- const collapsed = query.getCollapsed();
- const insertDatasource = query.getIngestDatasource();
-
- const statsTaskId: string | undefined = execution?.id;
-
- let extraInfo: string | undefined;
- if (collapsed && parsedQuery instanceof SqlQuery) {
- try {
- extraInfo =
summarizeExternalConfig(fitExternalConfigPattern(parsedQuery));
- } catch {}
- }
-
- const onUserCancel = (message?: string) => {
- queryManager.cancelCurrent(message);
- nativeQueryCancelFnRef.current?.();
- };
-
- return (
- <div className="helper-query">
- <div className="query-top-bar">
- <Button
- icon={collapsed ? IconNames.CARET_RIGHT : IconNames.CARET_DOWN}
- minimal
- onClick={() => onQueryChange(query.changeCollapsed(!collapsed))}
- />
- {insertDatasource ? (
- `<insert query : ${insertDatasource}>`
- ) : (
- <>
- {collapsed ? (
- <span className="query-name">{query.getQueryName()}</span>
- ) : (
- <InputGroup
- className="query-name"
- value={query.getQueryName()}
- onChange={(e: any) => {
- onQueryChange(query.changeQueryName(e.target.value));
- }}
- />
- )}
- <span className="as-label">AS</span>
- {extraInfo && <span className="extra-info">{extraInfo}</span>}
- </>
- )}
- <ButtonGroup className="corner">
- <Popover2
- content={
- <Menu>
- <MenuItem
- icon={IconNames.DUPLICATE}
- text="Duplicate"
- onClick={() => onQueryChange(query.duplicateLast())}
- />
- </Menu>
- }
- >
- <Button icon={IconNames.MORE} minimal />
- </Popover2>
- <Button
- icon={IconNames.CROSS}
- minimal
- onClick={() => {
- ExecutionStateCache.deleteState(id);
- WorkbenchRunningPromises.deletePromise(id);
- onDelete();
- }}
- />
- </ButtonGroup>
- </div>
- {!collapsed && (
- <>
- <FlexibleQueryInput
- ref={queryInputRef}
- autoHeight
- queryString={query.getQueryString()}
- onQueryStringChange={handleQueryStringChange}
- columnMetadata={
- columnMetadata ?
columnMetadata.concat(query.getInlineMetadata()) : undefined
- }
- editorStateId={query.getId()}
- />
- <div className="query-control-bar">
- <RunPanel
- query={query}
- onQueryChange={onQueryChange}
- onRun={handleRun}
- loading={executionState.loading}
- small
- queryEngines={queryEngines}
- clusterCapacity={clusterCapacity}
- />
- {executionState.isLoading() && (
- <ExecutionTimerPanel
- execution={executionState.intermediate}
- onCancel={() => queryManager.cancelCurrent()}
- />
- )}
- {(execution || executionState.error) && (
- <ExecutionSummaryPanel
- execution={execution}
- onExecutionDetail={() => onDetails(statsTaskId!)}
- onReset={() => {
- queryManager.reset();
- onQueryChange(props.query.changeLastExecution(undefined));
- ExecutionStateCache.deleteState(id);
- }}
- />
- )}
- </div>
- {!executionState.isInit() && (
- <div className="output-pane">
- {execution &&
- (execution.result ? (
- <ResultTablePane
- runeMode={execution.engine === 'native'}
- queryResult={execution.result}
- onQueryAction={handleQueryAction}
- initPageSize={5}
- />
- ) : execution.isSuccessfulInsert() ? (
- <IngestSuccessPane
- execution={execution}
- onDetails={onDetails}
- onQueryTab={onQueryTab}
- />
- ) : execution.error ? (
- <div className="error-container">
- <ExecutionErrorPane execution={execution} />
- {execution.stages && (
- <ExecutionStagesPane
- execution={execution}
- onErrorClick={() => onDetails(statsTaskId!, 'error')}
- onWarningClick={() => onDetails(statsTaskId!,
'warnings')}
- goToTask={goToTask}
- />
- )}
- </div>
- ) : (
- <div>Unknown query execution state</div>
- ))}
- {executionState.error && (
- <QueryErrorPane
- error={executionState.error}
- moveCursorTo={position => {
- moveToPosition(position);
- }}
- queryString={query.getQueryString()}
- onQueryStringChange={handleQueryStringChange}
- />
- )}
- {executionState.isLoading() &&
- (executionState.intermediate ? (
- <ExecutionProgressPane
- execution={executionState.intermediate}
- intermediateError={executionState.intermediateError}
- goToTask={goToTask}
- onCancel={onUserCancel}
- />
- ) : (
- <Loader cancelText="Cancel query" onCancel={onUserCancel} />
- ))}
- </div>
- )}
- </>
- )}
- {alertElement}
- </div>
- );
-});
diff --git a/web-console/src/views/workbench-view/query-tab/query-tab.scss
b/web-console/src/views/workbench-view/query-tab/query-tab.scss
index 31cf09489c..eee0375e0e 100644
--- a/web-console/src/views/workbench-view/query-tab/query-tab.scss
+++ b/web-console/src/views/workbench-view/query-tab/query-tab.scss
@@ -49,39 +49,20 @@ $vertical-gap: 6px;
width: 100%;
top: 0;
bottom: 30px + $vertical-gap;
- overflow: auto;
+ @include card-like;
+ overflow: hidden;
- .helper-query {
- margin-top: $vertical-gap;
+ .flexible-query-input {
+ height: 100%;
}
- .main-query {
- position: relative;
- @include card-like;
- min-height: 100%;
- overflow: hidden;
-
- &.single {
- height: 100%;
-
- .flexible-query-input {
- height: 100%;
- }
- }
-
- &.multi {
- min-height: calc(100% - 18px);
- margin-top: $vertical-gap;
- }
-
- .corner {
- position: absolute;
- top: 0;
- right: 0;
- @include card-background;
- z-index: 1;
- padding: 3px;
- }
+ .corner {
+ position: absolute;
+ top: 0;
+ right: 0;
+ @include card-background;
+ z-index: 1;
+ padding: 3px;
}
}
diff --git a/web-console/src/views/workbench-view/query-tab/query-tab.tsx
b/web-console/src/views/workbench-view/query-tab/query-tab.tsx
index e3d23a7ab6..8a4129fc67 100644
--- a/web-console/src/views/workbench-view/query-tab/query-tab.tsx
+++ b/web-console/src/views/workbench-view/query-tab/query-tab.tsx
@@ -16,13 +16,11 @@
* limitations under the License.
*/
-import { Button, Code, Intent, Menu, MenuItem } from '@blueprintjs/core';
+import { Code, Intent } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
-import { Popover2 } from '@blueprintjs/popover2';
import type { QueryResult } from '@druid-toolkit/query';
import { QueryRunner, SqlQuery } from '@druid-toolkit/query';
import axios from 'axios';
-import classNames from 'classnames';
import type { JSX } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import SplitterLayout from 'react-splitter-layout';
@@ -43,7 +41,7 @@ import { ExecutionStateCache } from
'../../../singletons/execution-state-cache';
import { WorkbenchHistory } from '../../../singletons/workbench-history';
import type { WorkbenchRunningPromise } from
'../../../singletons/workbench-running-promises';
import { WorkbenchRunningPromises } from
'../../../singletons/workbench-running-promises';
-import type { ColumnMetadata, QueryAction, RowColumn } from '../../../utils';
+import type { ColumnMetadata, QueryAction, QuerySlice, RowColumn } from
'../../../utils';
import {
DruidError,
localStorageGet,
@@ -59,7 +57,6 @@ import { ExecutionStagesPane } from
'../execution-stages-pane/execution-stages-p
import { ExecutionSummaryPanel } from
'../execution-summary-panel/execution-summary-panel';
import { ExecutionTimerPanel } from
'../execution-timer-panel/execution-timer-panel';
import { FlexibleQueryInput } from
'../flexible-query-input/flexible-query-input';
-import { HelperQuery } from '../helper-query/helper-query';
import { IngestSuccessPane } from '../ingest-success-pane/ingest-success-pane';
import { metadataStateStore } from '../metadata-state-store';
import { ResultTablePane } from '../result-table-pane/result-table-pane';
@@ -74,6 +71,7 @@ const queryRunner = new QueryRunner({
export interface QueryTabProps {
query: WorkbenchQuery;
+ id: string;
mandatoryQueryContext: QueryContext | undefined;
columnMetadata: readonly ColumnMetadata[] | undefined;
onQueryChange(newQuery: WorkbenchQuery): void;
@@ -88,6 +86,7 @@ export interface QueryTabProps {
export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
const {
query,
+ id,
columnMetadata,
mandatoryQueryContext,
onQueryChange,
@@ -145,7 +144,6 @@ export const QueryTab = React.memo(function QueryTab(props:
QueryTabProps) {
const queryInputRef = useRef<FlexibleQueryInput | null>(null);
- const id = query.getId();
const [executionState, queryManager] = useQueryManager<
WorkbenchQuery | WorkbenchRunningPromise | LastExecution,
Execution,
@@ -159,13 +157,13 @@ export const QueryTab = React.memo(function
QueryTab(props: QueryTabProps) {
processQuery: async (q, cancelToken) => {
if (q instanceof WorkbenchQuery) {
ExecutionStateCache.deleteState(id);
- const { engine, query, sqlPrefixLines, cancelQueryId } =
q.getApiQuery();
+ const { engine, query, prefixLines, cancelQueryId } = q.getApiQuery();
switch (engine) {
case 'sql-msq-task':
return await submitTaskQuery({
query,
- prefixLines: sqlPrefixLines,
+ prefixLines,
cancelToken,
preserveOnTermination: true,
onSubmitted: id => {
@@ -199,13 +197,13 @@ export const QueryTab = React.memo(function
QueryTab(props: QueryTabProps) {
nativeQueryCancelFnRef.current = cancelFn;
}),
});
- WorkbenchRunningPromises.storePromise(id, { promise:
resultPromise, sqlPrefixLines });
+ WorkbenchRunningPromises.storePromise(id, { promise:
resultPromise, prefixLines });
result = await resultPromise;
nativeQueryCancelFnRef.current = undefined;
} catch (e) {
nativeQueryCancelFnRef.current = undefined;
- throw new DruidError(e, sqlPrefixLines);
+ throw new DruidError(e, prefixLines);
}
return Execution.fromResult(engine, result);
@@ -217,7 +215,7 @@ export const QueryTab = React.memo(function QueryTab(props:
QueryTabProps) {
result = await q.promise;
} catch (e) {
WorkbenchRunningPromises.deletePromise(id);
- throw new DruidError(e, q.sqlPrefixLines);
+ throw new DruidError(e, q.prefixLines);
}
WorkbenchRunningPromises.deletePromise(id);
@@ -274,7 +272,7 @@ export const QueryTab = React.memo(function QueryTab(props:
QueryTabProps) {
currentQueryInput.goToPosition(position);
}
- const handleRun = usePermanentCallback(async (preview: boolean) => {
+ const handleRun = usePermanentCallback(async (preview: boolean, querySlice?:
QuerySlice) => {
const queryIssue = query.getIssue();
if (queryIssue) {
const position = WorkbenchQuery.getRowColumnFromIssue(queryIssue);
@@ -294,15 +292,22 @@ export const QueryTab = React.memo(function
QueryTab(props: QueryTabProps) {
return;
}
- if (query.getEffectiveEngine() !== 'sql-msq-task') {
- WorkbenchHistory.addQueryToHistory(query);
- queryManager.runQuery(query);
+ let effectiveQuery = query;
+ if (querySlice) {
+ effectiveQuery = effectiveQuery
+ .changeQueryString(querySlice.sql)
+ .changePrefixLines(querySlice.startRowColumn.row);
+ }
+
+ if (effectiveQuery.getEffectiveEngine() !== 'sql-msq-task') {
+ WorkbenchHistory.addQueryToHistory(effectiveQuery);
+ queryManager.runQuery(effectiveQuery);
return;
}
- const effectiveQuery = preview
- ? query.makePreview()
- : query.setMaxNumTasksIfUnset(clusterCapacity);
+ effectiveQuery = preview
+ ? effectiveQuery.makePreview()
+ : effectiveQuery.setMaxNumTasksIfUnset(clusterCapacity);
const capacityInfo = await maybeGetClusterCapacity();
@@ -327,9 +332,6 @@ export const QueryTab = React.memo(function QueryTab(props:
QueryTabProps) {
const statsTaskId: string | undefined = execution?.id;
- const queryPrefixes = query.getPrefixQueries();
- const extractedCtes = query.extractCteHelpers();
-
const onUserCancel = (message?: string) => {
queryManager.cancelCurrent(message);
nativeQueryCancelFnRef.current?.();
@@ -347,81 +349,22 @@ export const QueryTab = React.memo(function
QueryTab(props: QueryTabProps) {
>
<div className="top-section">
<div className="query-section">
- {queryPrefixes.map((queryPrefix, i) => (
- <HelperQuery
- key={queryPrefix.getId()}
- query={queryPrefix}
- mandatoryQueryContext={mandatoryQueryContext}
- columnMetadata={columnMetadata}
- onQueryChange={newQuery => {
- onQueryChange(query.applyUpdate(newQuery, i));
- }}
- onQueryTab={onQueryTab}
- onDelete={() => {
- onQueryChange(query.remove(i));
- }}
- onDetails={onDetails}
- queryEngines={queryEngines}
- clusterCapacity={clusterCapacity}
- goToTask={goToTask}
- />
- ))}
- <div className={classNames('main-query', queryPrefixes.length ?
'multi' : 'single')}>
- <FlexibleQueryInput
- ref={queryInputRef}
- autoHeight={Boolean(queryPrefixes.length)}
- minRows={10}
- queryString={query.getQueryString()}
- onQueryStringChange={handleQueryStringChange}
- columnMetadata={
- columnMetadata ?
columnMetadata.concat(query.getInlineMetadata()) : undefined
- }
- editorStateId={query.getId()}
- />
- <div className="corner">
- <Popover2
- content={
- <Menu>
- <MenuItem
- icon={IconNames.ARROW_UP}
- text="Save as helper query"
- onClick={() => {
- onQueryChange(query.addBlank());
- }}
- />
- {extractedCtes !== query && (
- <MenuItem
- icon={IconNames.DOCUMENT_SHARE}
- text="Extract WITH clauses into helper queries"
- onClick={() => onQueryChange(extractedCtes)}
- />
- )}
- {query.hasHelperQueries() && (
- <MenuItem
- icon={IconNames.DOCUMENT_OPEN}
- text="Materialize helper queries"
- onClick={() =>
onQueryChange(query.materializeHelpers())}
- />
- )}
- <MenuItem
- icon={IconNames.DUPLICATE}
- text="Duplicate as helper query"
- onClick={() => onQueryChange(query.duplicateLast())}
- />
- </Menu>
- }
- >
- <Button icon={IconNames.LIST} minimal />
- </Popover2>
- </div>
- </div>
+ <FlexibleQueryInput
+ ref={queryInputRef}
+ queryString={query.getQueryString()}
+ onQueryStringChange={handleQueryStringChange}
+ runQuerySlice={slice => void handleRun(false, slice)}
+ running={executionState.loading}
+ columnMetadata={columnMetadata}
+ editorStateId={id}
+ />
</div>
<div className="run-bar">
<RunPanel
query={query}
onQueryChange={onQueryChange}
onRun={handleRun}
- loading={executionState.loading}
+ running={executionState.loading}
queryEngines={queryEngines}
clusterCapacity={clusterCapacity}
moreMenu={runMoreMenu}
diff --git a/web-console/src/views/workbench-view/run-panel/run-panel.tsx
b/web-console/src/views/workbench-view/run-panel/run-panel.tsx
index c196cfc21e..d9b5a1a34c 100644
--- a/web-console/src/views/workbench-view/run-panel/run-panel.tsx
+++ b/web-console/src/views/workbench-view/run-panel/run-panel.tsx
@@ -85,7 +85,7 @@ const NAMED_TIMEZONES: string[] = [
export interface RunPanelProps {
query: WorkbenchQuery;
onQueryChange(query: WorkbenchQuery): void;
- loading: boolean;
+ running: boolean;
small?: boolean;
onRun(preview: boolean): void | Promise<void>;
queryEngines: DruidEngine[];
@@ -94,7 +94,7 @@ export interface RunPanelProps {
}
export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
- const { query, onQueryChange, onRun, moreMenu, loading, small, queryEngines,
clusterCapacity } =
+ const { query, onQueryChange, onRun, moreMenu, running, small, queryEngines,
clusterCapacity } =
props;
const [editContextDialogOpen, setEditContextDialogOpen] = useState(false);
const [customTimezoneDialogOpen, setCustomTimezoneDialogOpen] =
useState(false);
@@ -201,7 +201,7 @@ export const RunPanel = React.memo(function RunPanel(props:
RunPanelProps) {
<div className="run-panel">
<Button
className={effectiveEngine === 'native' ? 'rune-button' : undefined}
- disabled={loading}
+ disabled={running}
icon={IconNames.CARET_RIGHT}
onClick={() => void onRun(false)}
text="Run"
@@ -211,7 +211,7 @@ export const RunPanel = React.memo(function RunPanel(props:
RunPanelProps) {
/>
{ingestMode && (
<Button
- disabled={loading}
+ disabled={running}
icon={IconNames.EYE_OPEN}
onClick={() => void onRun(true)}
text="Preview"
diff --git a/web-console/src/views/workbench-view/workbench-view.tsx
b/web-console/src/views/workbench-view/workbench-view.tsx
index efd106f8b8..097dfd2e4a 100644
--- a/web-console/src/views/workbench-view/workbench-view.tsx
+++ b/web-console/src/views/workbench-view/workbench-view.tsx
@@ -64,10 +64,10 @@ import { WorkbenchHistoryDialog } from
'./workbench-history-dialog/workbench-his
import './workbench-view.scss';
function cleanupTabEntry(tabEntry: TabEntry): void {
- const discardedIds = tabEntry.query.getIds();
- WorkbenchRunningPromises.deletePromises(discardedIds);
- ExecutionStateCache.deleteStates(discardedIds);
- AceEditorStateCache.deleteStates(discardedIds);
+ const discardedId = tabEntry.id;
+ WorkbenchRunningPromises.deletePromise(discardedId);
+ ExecutionStateCache.deleteState(discardedId);
+ AceEditorStateCache.deleteState(discardedId);
}
function externalDataTabId(tabId: string | undefined): boolean {
@@ -496,7 +496,7 @@ export class WorkbenchView extends
React.PureComponent<WorkbenchViewProps, Workb
const newTabEntry: TabEntry = {
id,
tabName: tabEntry.tabName + ' (copy)',
- query: tabEntry.query.duplicate(),
+ query: tabEntry.query,
};
this.handleQueriesChange(
tabEntries.slice(0, i + 1).concat(newTabEntry,
tabEntries.slice(i + 1)),
@@ -639,6 +639,7 @@ export class WorkbenchView extends
React.PureComponent<WorkbenchViewProps, Workb
<QueryTab
key={currentTabEntry.id}
query={currentTabEntry.query}
+ id={currentTabEntry.id}
mandatoryQueryContext={mandatoryQueryContext}
columnMetadata={columnMetadataState.getSomeData()}
onQueryChange={this.handleQueryChange}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]