diff --git a/web-console/src/components/fancy-numeric-input/fancy-numeric-input.tsx b/web-console/src/components/fancy-numeric-input/fancy-numeric-input.tsx index ad38196caed..de6bc510708 100644 --- a/web-console/src/components/fancy-numeric-input/fancy-numeric-input.tsx +++ b/web-console/src/components/fancy-numeric-input/fancy-numeric-input.tsx @@ -80,6 +80,7 @@ export interface FancyNumericInputProps { minorStepSize?: number; stepSize?: number; majorStepSize?: number; + arbitraryPrecision?: boolean; } export const FancyNumericInput = React.memo(function FancyNumericInput( @@ -103,6 +104,7 @@ export const FancyNumericInput = React.memo(function FancyNumericInput( min, max, + arbitraryPrecision, } = props; const stepSize = props.stepSize || 1; @@ -110,8 +112,11 @@ export const FancyNumericInput = React.memo(function FancyNumericInput( const majorStepSize = props.majorStepSize || stepSize * 10; function roundAndClamp(n: number): number { - const inv = 1 / minorStepSize; - return clamp(Math.floor(n * inv) / inv, min, max); + if (!arbitraryPrecision) { + const inv = 1 / minorStepSize; + n = Math.floor(n * inv) / inv; + } + return clamp(n, min, max); } const effectiveValue = value ?? defaultValue; diff --git a/web-console/src/components/table-cell/table-cell.scss b/web-console/src/components/table-cell/table-cell.scss index 62c80d46635..03eef53a993 100644 --- a/web-console/src/components/table-cell/table-cell.scss +++ b/web-console/src/components/table-cell/table-cell.scss @@ -20,6 +20,7 @@ .table-cell { padding: $table-cell-v-padding $table-cell-h-padding; + white-space: pre; &.null, &.empty { diff --git a/web-console/src/dialogs/kill-datasource-dialog/kill-datasource-dialog.tsx b/web-console/src/dialogs/kill-datasource-dialog/kill-datasource-dialog.tsx index 3eb7e9fdf24..f95a5a5d3b8 100644 --- a/web-console/src/dialogs/kill-datasource-dialog/kill-datasource-dialog.tsx +++ b/web-console/src/dialogs/kill-datasource-dialog/kill-datasource-dialog.tsx @@ -92,7 +92,7 @@ export const KillDatasourceDialog = function KillDatasourceDialog( format.

- If you have streaming ingestion running make sure that your interval range doe not + If you have streaming ingestion running make sure that your interval range does not overlap with intervals where streaming data is being added - otherwise the kill task will not start.

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 6fe223847f6..d7847287fa6 100644 --- a/web-console/src/druid-models/workbench-query/workbench-query.ts +++ b/web-console/src/druid-models/workbench-query/workbench-query.ts @@ -17,6 +17,7 @@ */ import type { + QueryParameter, SqlClusteredByClause, SqlExpression, SqlPartitionedByClause, @@ -66,6 +67,7 @@ interface IngestionLines { export interface WorkbenchQueryValue { queryString: string; queryContext: QueryContext; + queryParameters?: QueryParameter[]; engine?: DruidEngine; lastExecution?: LastExecution; unlimited?: boolean; @@ -235,6 +237,7 @@ export class WorkbenchQuery { public readonly queryString: string; public readonly queryContext: QueryContext; + public readonly queryParameters?: QueryParameter[]; public readonly engine?: DruidEngine; public readonly lastExecution?: LastExecution; public readonly unlimited?: boolean; @@ -251,6 +254,7 @@ export class WorkbenchQuery { } this.queryString = queryString; this.queryContext = value.queryContext; + this.queryParameters = value.queryParameters; // Start back compat code for the engine names that might be coming from local storage let possibleEngine: string | undefined = value.engine; @@ -274,6 +278,7 @@ export class WorkbenchQuery { return { queryString: this.queryString, queryContext: this.queryContext, + queryParameters: this.queryParameters, engine: this.engine, unlimited: this.unlimited, }; @@ -297,6 +302,10 @@ export class WorkbenchQuery { return new WorkbenchQuery({ ...this.valueOf(), queryContext }); } + public changeQueryParameters(queryParameters: QueryParameter[] | undefined): WorkbenchQuery { + return new WorkbenchQuery({ ...this.valueOf(), queryParameters }); + } + public changeEngine(engine: DruidEngine | undefined): WorkbenchQuery { return new WorkbenchQuery({ ...this.valueOf(), engine }); } @@ -425,11 +434,12 @@ export class WorkbenchQuery { let ret: WorkbenchQuery = this; // Explicitly select MSQ, adjust the context, set maxNumTasks to the lowest possible and add in ingest mode flags + const { queryContext } = this; ret = ret.changeEngine('sql-msq-task').changeQueryContext({ - ...this.queryContext, + ...queryContext, maxNumTasks: 2, - finalizeAggregations: false, - groupByEnableMultiValueUnnesting: false, + finalizeAggregations: queryContext.finalizeAggregations ?? false, + groupByEnableMultiValueUnnesting: queryContext.groupByEnableMultiValueUnnesting ?? false, }); // Remove everything pertaining to INSERT INTO / REPLACE INTO from the query string @@ -458,7 +468,7 @@ export class WorkbenchQuery { prefixLines: number; cancelQueryId?: string; } { - const { queryString, queryContext, unlimited, prefixLines } = this; + const { queryString, queryContext, queryParameters, unlimited, prefixLines } = this; const engine = this.getEffectiveEngine(); if (engine === 'native') { @@ -544,6 +554,10 @@ export class WorkbenchQuery { apiQuery.context.groupByEnableMultiValueUnnesting ??= !ingestQuery; } + if (Array.isArray(queryParameters) && queryParameters.length) { + apiQuery.parameters = queryParameters; + } + return { engine, query: apiQuery, diff --git a/web-console/src/utils/index.tsx b/web-console/src/utils/index.tsx index c27a070b976..e7dc33e7a65 100644 --- a/web-console/src/utils/index.tsx +++ b/web-console/src/utils/index.tsx @@ -29,7 +29,6 @@ export * from './local-storage-backed-visibility'; export * from './local-storage-keys'; export * from './object-change'; export * from './query-action'; -export * from './query-cursor'; export * from './query-manager'; export * from './query-state'; export * from './sample-query'; diff --git a/web-console/src/utils/query-cursor.ts b/web-console/src/utils/query-cursor.ts deleted file mode 100644 index 94bbe179389..00000000000 --- a/web-console/src/utils/query-cursor.ts +++ /dev/null @@ -1,55 +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 { 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 = '$.X.@.X.$'; -const DOT_DOT_DOT_LITERAL = L('...'); - -export function prettyPrintSql(b: SqlBase): string { - return b - .walk(b => { - if (b === EMPTY_LITERAL) { - return DOT_DOT_DOT_LITERAL; - } - return b; - }) - .prettyTrim(50) - .toString(); -} - -export function findEmptyLiteralPosition(query: SqlQuery): RowColumn | undefined { - const subQueryString = query.walk(b => (b === EMPTY_LITERAL ? L(CRAZY_STRING) : b)).toString(); - - const crazyIndex = subQueryString.indexOf(CRAZY_STRING); - if (crazyIndex < 0) return; - - const prefix = subQueryString.slice(0, crazyIndex); - const lines = prefix.split(/\n/g); - const row = lines.length - 1; - const lastLine = lines[row]; - return { - row: row, - column: lastLine.length, - }; -} diff --git a/web-console/src/utils/sql.ts b/web-console/src/utils/sql.ts index 7404ee31372..61cd4c7ed41 100644 --- a/web-console/src/utils/sql.ts +++ b/web-console/src/utils/sql.ts @@ -16,6 +16,7 @@ * limitations under the License. */ +import type { SqlBase } from '@druid-toolkit/query'; import { SqlColumn, SqlExpression, @@ -28,6 +29,10 @@ import { import type { RowColumn } from './general'; import { offsetToRowColumn } from './general'; +export function prettyPrintSql(b: SqlBase): string { + return b.prettyTrim(50).toString(); +} + export function timeFormatToSql(timeFormat: string): SqlExpression | undefined { switch (timeFormat) { case 'auto': diff --git a/web-console/src/utils/types.ts b/web-console/src/utils/types.ts index ed192eff6e8..d164d46138b 100644 --- a/web-console/src/utils/types.ts +++ b/web-console/src/utils/types.ts @@ -89,6 +89,9 @@ export function dataTypeToIcon(dataType: string): IconName { case 'COMPLEX': return IconNames.IP_ADDRESS; + case 'COMPLEX': + return IconNames.DOUBLE_CHEVRON_RIGHT; + case 'NULL': return IconNames.CIRCLE; diff --git a/web-console/src/views/datasources-view/datasources-view.tsx b/web-console/src/views/datasources-view/datasources-view.tsx index 570a55a87bc..75541b82999 100644 --- a/web-console/src/views/datasources-view/datasources-view.tsx +++ b/web-console/src/views/datasources-view/datasources-view.tsx @@ -383,8 +383,8 @@ export class DatasourcesView extends React.PureComponent< return `SELECT ${columns.join(',\n')} FROM sys.segments -GROUP BY 1 -ORDER BY 1`; +GROUP BY datasource +ORDER BY datasource`; } static RUNNING_TASK_SQL = `SELECT diff --git a/web-console/src/views/explore-view/modules/table-react-module.tsx b/web-console/src/views/explore-view/modules/table-react-module.tsx index 459e28014a8..dabe6217c9b 100644 --- a/web-console/src/views/explore-view/modules/table-react-module.tsx +++ b/web-console/src/views/explore-view/modules/table-react-module.tsx @@ -16,12 +16,11 @@ * limitations under the License. */ -import type { SqlOrderByExpression } from '@druid-toolkit/query'; +import type { SqlColumn, SqlOrderByExpression } from '@druid-toolkit/query'; import { C, F, SqlCase, - SqlColumn, SqlExpression, SqlFunction, SqlLiteral, @@ -80,22 +79,24 @@ function nullableColumn(column: ExpressionMeta) { } function nvl(ex: SqlExpression): SqlExpression { - return SqlFunction.simple('NVL', [ex, NULL_REPLACEMENT]); + return SqlFunction.simple('NVL', [ex.cast('VARCHAR'), NULL_REPLACEMENT]); } -function nullif(ex: SqlExpression): SqlExpression { - return SqlFunction.simple('NULLIF', [ex, NULL_REPLACEMENT]); +function joinEquals(c1: SqlColumn, c2: SqlColumn, nullable: boolean): SqlExpression { + return c1.applyIf(nullable, nvl).equal(c2.applyIf(nullable, nvl)); } function toGroupByExpression( splitColumn: ExpressionMeta, - nvlIfNeeded: boolean, timeBucket: string, + compareShiftDuration?: string, ) { const { expression, sqlType, name } = splitColumn; return expression - .applyIf(sqlType === 'TIMESTAMP', e => SqlFunction.simple('TIME_FLOOR', [e, timeBucket])) - .applyIf(nvlIfNeeded && nullableColumn(splitColumn), nvl) + .applyIf(sqlType === 'TIMESTAMP' && compareShiftDuration, e => + F.timeShift(e, compareShiftDuration!, 1), + ) + .applyIf(sqlType === 'TIMESTAMP', e => F.timeFloor(e, timeBucket)) .as(name); } @@ -143,16 +144,6 @@ function toShowColumnExpression( return ex.as(showColumn.name); } -function shiftTime(ex: SqlQuery, period: string): SqlQuery { - return ex.walk(q => { - if (q instanceof SqlColumn && q.getName() === '__time') { - return SqlFunction.simple('TIME_SHIFT', [q, period, 1]); - } else { - return q; - } - }) as SqlQuery; -} - interface QueryAndHints { query: SqlQuery; groupHints: string[]; @@ -327,7 +318,7 @@ function TableModule(props: TableModuleProps) { const mainQuery = getInitQuery(table, where) .applyForEach(splitColumns, (q, splitColumn) => - q.addSelect(toGroupByExpression(splitColumn, hasCompare, timeBucket), { + q.addSelect(toGroupByExpression(splitColumn, timeBucket), { addToGroupBy: 'end', }), ) @@ -381,26 +372,20 @@ function TableModule(props: TableModuleProps) { `compare${i}`, getInitQuery(table, where) .applyForEach(splitColumns, (q, splitColumn) => - q.addSelect(toGroupByExpression(splitColumn, true, timeBucket), { + q.addSelect(toGroupByExpression(splitColumn, timeBucket, comparePeriod), { addToGroupBy: 'end', }), ) .applyForEach(metrics, (q, metric) => q.addSelect(metric.expression.as(metric.name)), - ) - .apply(q => shiftTime(q, comparePeriod)), + ), ), ), ), ) .changeSelectExpressions( splitColumns - .map(splitColumn => - main - .column(splitColumn.name) - .applyIf(nullableColumn(splitColumn), nullif) - .as(splitColumn.name), - ) + .map(splitColumn => main.column(splitColumn.name).as(splitColumn.name)) .concat( showColumns.map(showColumn => main.column(showColumn.name).as(showColumn.name)), metrics.map(metric => main.column(metric.name).as(metric.name)), @@ -432,7 +417,11 @@ function TableModule(props: TableModuleProps) { T(`compare${i}`), SqlExpression.and( ...splitColumns.map(splitColumn => - main.column(splitColumn.name).equal(T(`compare${i}`).column(splitColumn.name)), + joinEquals( + main.column(splitColumn.name), + T(`compare${i}`).column(splitColumn.name), + nullableColumn(splitColumn), + ), ), ), ), diff --git a/web-console/src/views/workbench-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx b/web-console/src/views/workbench-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx index db566765d3d..a32db7f8b65 100644 --- a/web-console/src/views/workbench-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx +++ b/web-console/src/views/workbench-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx @@ -19,11 +19,11 @@ import { MenuItem } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import type { SqlExpression, SqlQuery } from '@druid-toolkit/query'; -import { C, F, N, SqlJoinPart, T } from '@druid-toolkit/query'; +import { C, F, N, SqlJoinPart, SqlPlaceholder, T } from '@druid-toolkit/query'; import type { JSX } from 'react'; import React from 'react'; -import { EMPTY_LITERAL, prettyPrintSql } from '../../../../../utils'; +import { prettyPrintSql } from '../../../../../utils'; import { getJoinColumns } from '../../column-tree'; export interface StringMenuItemsProps { @@ -53,9 +53,9 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String return ( {filterMenuItem(column.isNotNull())} - {filterMenuItem(column.equal(EMPTY_LITERAL), false)} - {filterMenuItem(column.like(EMPTY_LITERAL), false)} - {filterMenuItem(F('REGEXP_LIKE', column, EMPTY_LITERAL), false)} + {filterMenuItem(column.equal(SqlPlaceholder.PLACEHOLDER), false)} + {filterMenuItem(column.like(SqlPlaceholder.PLACEHOLDER), false)} + {filterMenuItem(F('REGEXP_LIKE', column, SqlPlaceholder.PLACEHOLDER), false)} ); } @@ -136,7 +136,7 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String {aggregateMenuItem(F.countDistinct(column), `dist_${columnName}`)} {aggregateMenuItem( - F.count().addWhereExpression(column.equal(EMPTY_LITERAL)), + F.count().addWhereExpression(column.equal(SqlPlaceholder.PLACEHOLDER)), `filtered_dist_${columnName}`, false, )} diff --git a/web-console/src/views/workbench-view/query-parameters-dialog/__snapshots__/query-parameters-dialog.spec.tsx.snap b/web-console/src/views/workbench-view/query-parameters-dialog/__snapshots__/query-parameters-dialog.spec.tsx.snap new file mode 100644 index 00000000000..3ea1e60838b --- /dev/null +++ b/web-console/src/views/workbench-view/query-parameters-dialog/__snapshots__/query-parameters-dialog.spec.tsx.snap @@ -0,0 +1,457 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`QueryParametersDialog matches snapshot 1`] = ` + +
+

+ Druid SQL supports dynamic parameters using question mark + + ? + + syntax, where parameters are bound positionally to ? placeholders at execution time. +

+ + + + + + + + + + } + defaultIsOpen={false} + disabled={false} + fill={false} + hasBackdrop={false} + hoverCloseDelay={300} + hoverOpenDelay={150} + inheritDarkTheme={true} + interactionKind="click" + matchTargetWidth={false} + minimal={true} + openOnTargetFocus={true} + position="bottom-left" + positioningStrategy="absolute" + shouldReturnFocusOnClose={false} + targetTagName="span" + transitionDuration={300} + usePortal={true} + > + + + + + + + + + + + + + + + + } + defaultIsOpen={false} + disabled={false} + fill={false} + hasBackdrop={false} + hoverCloseDelay={300} + hoverOpenDelay={150} + inheritDarkTheme={true} + interactionKind="click" + matchTargetWidth={false} + minimal={true} + openOnTargetFocus={true} + position="bottom-left" + positioningStrategy="absolute" + shouldReturnFocusOnClose={false} + targetTagName="span" + transitionDuration={300} + usePortal={true} + > + + + + + + + + + + + + + + + + } + defaultIsOpen={false} + disabled={false} + fill={false} + hasBackdrop={false} + hoverCloseDelay={300} + hoverOpenDelay={150} + inheritDarkTheme={true} + interactionKind="click" + matchTargetWidth={false} + minimal={true} + openOnTargetFocus={true} + position="bottom-left" + positioningStrategy="absolute" + shouldReturnFocusOnClose={false} + targetTagName="span" + transitionDuration={300} + usePortal={true} + > + + + + + + + + + + + + + + + + } + defaultIsOpen={false} + disabled={false} + fill={false} + hasBackdrop={false} + hoverCloseDelay={300} + hoverOpenDelay={150} + inheritDarkTheme={true} + interactionKind="click" + matchTargetWidth={false} + minimal={true} + openOnTargetFocus={true} + position="bottom-left" + positioningStrategy="absolute" + shouldReturnFocusOnClose={false} + targetTagName="span" + transitionDuration={300} + usePortal={true} + > + + + + + + + +
+
+
+ + +
+
+
+`; diff --git a/web-console/src/views/workbench-view/query-parameters-dialog/query-parameters-dialog.scss b/web-console/src/views/workbench-view/query-parameters-dialog/query-parameters-dialog.scss new file mode 100644 index 00000000000..caa7f5445c8 --- /dev/null +++ b/web-console/src/views/workbench-view/query-parameters-dialog/query-parameters-dialog.scss @@ -0,0 +1,28 @@ +/* + * 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'; + +.query-parameters-dialog { + .#{$bp-ns}-dialog-body { + position: relative; + min-height: 50vh; + overflow: auto; + max-height: 80vh; + } +} diff --git a/web-console/src/views/workbench-view/query-parameters-dialog/query-parameters-dialog.spec.tsx b/web-console/src/views/workbench-view/query-parameters-dialog/query-parameters-dialog.spec.tsx new file mode 100644 index 00000000000..539b399cdd6 --- /dev/null +++ b/web-console/src/views/workbench-view/query-parameters-dialog/query-parameters-dialog.spec.tsx @@ -0,0 +1,42 @@ +/* + * 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 React from 'react'; + +import { shallow } from '../../../utils/shallow-renderer'; + +import { QueryParametersDialog } from './query-parameters-dialog'; + +describe('QueryParametersDialog', () => { + it('matches snapshot', () => { + const comp = shallow( + {}} + onClose={() => {}} + />, + ); + + expect(comp).toMatchSnapshot(); + }); +}); diff --git a/web-console/src/views/workbench-view/query-parameters-dialog/query-parameters-dialog.tsx b/web-console/src/views/workbench-view/query-parameters-dialog/query-parameters-dialog.tsx new file mode 100644 index 00000000000..6a6efda2f2c --- /dev/null +++ b/web-console/src/views/workbench-view/query-parameters-dialog/query-parameters-dialog.tsx @@ -0,0 +1,152 @@ +/* + * 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, + Classes, + Code, + ControlGroup, + Dialog, + FormGroup, + InputGroup, + Intent, + Menu, + MenuItem, + Position, +} from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { Popover2 } from '@blueprintjs/popover2'; +import type { QueryParameter } from '@druid-toolkit/query'; +import { isEmptyArray } from '@druid-toolkit/query'; +import React, { useState } from 'react'; + +import { FancyNumericInput } from '../../../components/fancy-numeric-input/fancy-numeric-input'; +import { deepSet, oneOf, tickIcon, without } from '../../../utils'; + +import './query-parameters-dialog.scss'; + +const TYPES = ['VARCHAR', 'TIMESTAMP', 'BIGINT', 'DOUBLE', 'FLOAT']; + +interface QueryParametersDialogProps { + queryParameters: QueryParameter[] | undefined; + onQueryParametersChange(parameters: QueryParameter[] | undefined): void; + onClose(): void; +} + +export const QueryParametersDialog = React.memo(function QueryParametersDialog( + props: QueryParametersDialogProps, +) { + const { queryParameters, onQueryParametersChange, onClose } = props; + const [currentQueryParameters, setCurrentQueryParameters] = useState(queryParameters || []); + + function onSave() { + onQueryParametersChange( + isEmptyArray(currentQueryParameters) ? undefined : currentQueryParameters, + ); + onClose(); + } + + return ( + +
+

+ Druid SQL supports dynamic parameters using question mark ? syntax, where + parameters are bound positionally to ? placeholders at execution time. +

+ {currentQueryParameters.map((queryParameter, i) => { + const { type, value } = queryParameter; + + function onValueChange(v: string | number) { + setCurrentQueryParameters(deepSet(currentQueryParameters, `${i}.value`, v)); + } + + return ( + + + + {TYPES.map(t => ( + { + setCurrentQueryParameters( + deepSet(currentQueryParameters, `${i}.type`, t), + ); + }} + /> + ))} + + } + > +
+
+
+
+
+
+ ); +}); 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 d9b5a1a34c0..ec1b95ad38a 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 @@ -57,6 +57,7 @@ import { } from '../../../druid-models'; import { deepGet, deepSet, pluralIfNeeded, tickIcon } from '../../../utils'; import { MaxTasksButton } from '../max-tasks-button/max-tasks-button'; +import { QueryParametersDialog } from '../query-parameters-dialog/query-parameters-dialog'; import './run-panel.scss'; @@ -97,6 +98,7 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) { const { query, onQueryChange, onRun, moreMenu, running, small, queryEngines, clusterCapacity } = props; const [editContextDialogOpen, setEditContextDialogOpen] = useState(false); + const [editParametersDialogOpen, setEditParametersDialogOpen] = useState(false); const [customTimezoneDialogOpen, setCustomTimezoneDialogOpen] = useState(false); const [indexSpecDialogSpec, setIndexSpecDialogSpec] = useState(); @@ -104,6 +106,7 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) { const ingestMode = query.isIngestQuery(); const queryContext = query.queryContext; const numContextKeys = Object.keys(queryContext).length; + const queryParameters = query.queryParameters; const maxParseExceptions = getMaxParseExceptions(queryContext); const finalizeAggregations = getFinalizeAggregations(queryContext); @@ -238,6 +241,12 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) { onClick={() => setEditContextDialogOpen(true)} label={pluralIfNeeded(numContextKeys, 'key')} /> + setEditParametersDialogOpen(true)} + label={queryParameters ? pluralIfNeeded(queryParameters.length, 'parameter') : ''} + /> {effectiveEngine !== 'native' && ( )} + {editParametersDialogOpen && ( + onQueryChange(query.changeQueryParameters(p))} + onClose={() => { + setEditParametersDialogOpen(false); + }} + /> + )} {customTimezoneDialogOpen && (