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 3e9c628f084 feat: Web console: highlight NULLS FIRST / NULLS LAST and
upgrade druid-query-toolkit to 1.7.0 (#20621)
3e9c628f084 is described below
commit 3e9c628f0844657cfd3a64eccb591b5b9ae34daf
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Mon Oct 19 15:17:43 2026 -0700
feat: Web console: highlight NULLS FIRST / NULLS LAST and upgrade
druid-query-toolkit to 1.7.0 (#20621)
* Web console: upgrade druid-query-toolkit to 1.7.0
- Bump druid-query-toolkit to ^1.7.0 and widen query types to SqlQueryBase
where the toolkit now uses it (parseSqlQuery, result table, preview table,
cell filter menu)
- Add "node" to tsconfig types: the old toolkit typings leaked Node globals
- Override js-yaml for @istanbuljs/load-nyc-config to drop vulnerable
sprintf-js
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Web console: highlight and complete NULLS FIRST / NULLS LAST
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Web console: fix line numbers duplicating when a sub query run button
goes away
The gutter removes a single node per marker, but SubQueryMarker.toDOM
returned
a fragment of two nodes, so the line number text was left behind every time
the marker was removed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Web console: do not suggest ROW / ROWS after NULLS FIRST
FIRST is followed by ROW / ROWS in FETCH FIRST but ends the clause in
NULLS FIRST. Follow-up keyword suggestions can now be keyed by the two
keywords before the cursor, which take precedence over the one keyword.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Web console: suggest keywords based on the two keywords before the cursor
Uses the keyword pairs to tell apart GROUP BY / PARTITIONED BY / ORDER BY,
SELECT ALL / UNION ALL / PARTITIONED BY ALL, SELECT DISTINCT / IS DISTINCT,
CURRENT ROW / FETCH FIRST ROW and EXPLAIN PLAN FOR / SUBSTRING(... FOR ...).
Also suggests JOIN after OUTER and DISTINCT after IS NOT.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
web-console/package-lock.json | 56 ++--------------------
web-console/package.json | 7 ++-
.../cell-filter-menu/cell-filter-menu.tsx | 4 +-
web-console/src/components/code-editor/README.md | 3 ++
.../src/druid-models/execution/execution.spec.ts | 2 +-
.../src/druid-models/execution/execution.ts | 12 ++---
.../src/editor-completions/sql-completions.spec.ts | 55 +++++++++++++++++++++
.../src/editor-completions/sql-completions.ts | 50 +++++++++++++++++--
web-console/src/editor-languages/dsql-keywords.ts | 2 +
.../src/editor-languages/editor-languages.spec.ts | 15 ++++++
.../grouping-table-module.tsx | 4 +-
.../schema-step/preview-table/preview-table.tsx | 4 +-
.../flexible-query-input/sub-query-markers.spec.ts | 22 +++++++++
.../flexible-query-input/sub-query-markers.ts | 8 ++--
.../result-table-pane/result-table-pane.tsx | 8 +++-
web-console/tsconfig.json | 2 +-
16 files changed, 177 insertions(+), 77 deletions(-)
diff --git a/web-console/package-lock.json b/web-console/package-lock.json
index 59914166fac..0fc3df4b3ee 100644
--- a/web-console/package-lock.json
+++ b/web-console/package-lock.json
@@ -38,7 +38,7 @@
"d3-shape": "^3.2.0",
"d3-time-format": "^4.1.0",
"dayjs": "^1.11.15",
- "druid-query-toolkit": "^1.2.2",
+ "druid-query-toolkit": "^1.7.0",
"echarts": "^6.1.0",
"file-saver": "^2.0.5",
"hjson": "^3.2.2",
@@ -5270,16 +5270,6 @@
"node": ">=8"
}
},
- "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
- "integrity":
"sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "sprintf-js": "~1.0.2"
- }
- },
"node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
@@ -5294,20 +5284,6 @@
"node": ">=8"
}
},
- "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
- "version": "3.15.2",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz",
- "integrity":
"sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "argparse": "^1.0.7",
- "esprima": "^4.0.0"
- },
- "bin": {
- "js-yaml": "bin/js-yaml.js"
- }
- },
"node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": {
"version": "5.0.0",
"resolved":
"https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -11632,13 +11608,10 @@
}
},
"node_modules/druid-query-toolkit": {
- "version": "1.2.2",
- "resolved":
"https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-1.2.2.tgz",
- "integrity":
"sha512-725nxfbbq/AXGvzzRi+8j5v5/jTkAMVmipGxt57ATApMs0lmTN01+BWI4TuOiyD7OrmHyoi7huEy0xfQ9QpPxg==",
+ "version": "1.7.0",
+ "resolved":
"https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-1.7.0.tgz",
+ "integrity":
"sha512-V63vhsQQ/UqrCWmH/ipk7I0EDrqJ53bnYKwe7Nw2e1LyE/fJha+/pVVj41l+TczGjtbaryEp8o+gbZy9JUDCBg==",
"license": "Apache-2.0",
- "dependencies": {
- "tslib": "^2.5.2"
- },
"engines": {
"node": ">= 14.17.0"
}
@@ -12486,20 +12459,6 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/esprima": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
- "integrity":
"sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
- "dev": true,
- "license": "BSD-2-Clause",
- "bin": {
- "esparse": "bin/esparse.js",
- "esvalidate": "bin/esvalidate.js"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/esquery": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
@@ -20461,13 +20420,6 @@
"spdx-license-ids": "^3.0.0"
}
},
- "node_modules/sprintf-js": {
- "version": "1.0.3",
- "resolved":
"https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
- "integrity":
"sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
- "dev": true,
- "license": "BSD-3-Clause"
- },
"node_modules/ssri": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz",
diff --git a/web-console/package.json b/web-console/package.json
index 2fd415f6913..7863d4905dc 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -76,7 +76,7 @@
"d3-shape": "^3.2.0",
"d3-time-format": "^4.1.0",
"dayjs": "^1.11.15",
- "druid-query-toolkit": "^1.2.2",
+ "druid-query-toolkit": "^1.7.0",
"echarts": "^6.1.0",
"file-saver": "^2.0.5",
"hjson": "^3.2.2",
@@ -151,5 +151,10 @@
"playwright-chromium": true,
"@parcel/watcher": false,
"unrs-resolver": false
+ },
+ "overrides": {
+ "@istanbuljs/load-nyc-config": {
+ "js-yaml": "^4.1.1"
+ }
}
}
diff --git a/web-console/src/components/cell-filter-menu/cell-filter-menu.tsx
b/web-console/src/components/cell-filter-menu/cell-filter-menu.tsx
index 5d2098c7db4..95766dac248 100644
--- a/web-console/src/components/cell-filter-menu/cell-filter-menu.tsx
+++ b/web-console/src/components/cell-filter-menu/cell-filter-menu.tsx
@@ -18,7 +18,7 @@
import { Menu, MenuItem } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
-import type { Column, SqlExpression, SqlQuery } from 'druid-query-toolkit';
+import type { Column, SqlExpression, SqlQueryBase } from 'druid-query-toolkit';
import { C, L, SqlComparison, SqlLiteral, SqlRecord, trimString } from
'druid-query-toolkit';
import type { QueryAction } from '../../utils';
@@ -80,7 +80,7 @@ export interface CellFilterMenuProps {
value: unknown;
headerIndex: number;
runeMode?: boolean;
- query: SqlQuery | undefined;
+ query: SqlQueryBase | undefined;
onQueryAction?(action: QueryAction): void;
onShowFullValue?(valueString: string): void;
}
diff --git a/web-console/src/components/code-editor/README.md
b/web-console/src/components/code-editor/README.md
index 29a7a81317e..c9e88d90cd8 100644
--- a/web-console/src/components/code-editor/README.md
+++ b/web-console/src/components/code-editor/README.md
@@ -223,6 +223,9 @@ made of:
- `lineNumbers({ domEventHandlers })` for the click and hover, which hands
over the line that was clicked or hovered
- a `StateEffect` to set the hovered query
+A gutter marker's `toDOM` must return a single node (not a
`DocumentFragment`): the gutter removes just one node when
+the marker goes away, so any extra nodes would be left behind.
+
React only provides `onRun`. Because `extensions` is only read once,
`FlexibleQueryInput` puts the markers in its own
`Compartment` and reconfigures it when they are turned on or off.
diff --git a/web-console/src/druid-models/execution/execution.spec.ts
b/web-console/src/druid-models/execution/execution.spec.ts
index 1c3f25e3595..50b47bf101a 100644
--- a/web-console/src/druid-models/execution/execution.spec.ts
+++ b/web-console/src/druid-models/execution/execution.spec.ts
@@ -641,7 +641,7 @@ describe('Execution', () => {
"id": "query-45f1dafd-8a52-4eb7-9a6c-77840cddd349",
"nativeQuery": undefined,
"queryContext": undefined,
- "result": _QueryResult {
+ "result": QueryResult {
"header": [
Column {
"name": "channel",
diff --git a/web-console/src/druid-models/execution/execution.ts
b/web-console/src/druid-models/execution/execution.ts
index f26b739b445..6a0835cfd21 100644
--- a/web-console/src/druid-models/execution/execution.ts
+++ b/web-console/src/druid-models/execution/execution.ts
@@ -16,7 +16,7 @@
* limitations under the License.
*/
-import { Column, QueryResult, SqlExpression, SqlQuery, SqlWithQuery } from
'druid-query-toolkit';
+import { Column, QueryResult, SqlExpression, SqlQueryBase } from
'druid-query-toolkit';
import { maybeGetClusterCapacity } from '../../helpers';
import {
@@ -52,13 +52,11 @@ const IGNORE_CONTEXT_KEYS = [
'sqlReplaceTimeChunks',
];
-// Hack around the concept that we might get back a SqlWithQuery and will need
to unpack it
-function parseSqlQuery(queryString: string): SqlQuery | undefined {
+// Parse any query form, flattening a WITH query so that its select
expressions line up with the result columns
+function parseSqlQuery(queryString: string): SqlQueryBase | undefined {
const q = SqlExpression.maybeParse(queryString);
- if (!q) return;
- if (q instanceof SqlWithQuery) return q.flattenWith();
- if (q instanceof SqlQuery) return q;
- return;
+ if (!(q instanceof SqlQueryBase)) return;
+ return q.flattenWith();
}
export interface ExecutionError {
diff --git a/web-console/src/editor-completions/sql-completions.spec.ts
b/web-console/src/editor-completions/sql-completions.spec.ts
index ac0bc8d1f36..3f2b6b6c419 100644
--- a/web-console/src/editor-completions/sql-completions.spec.ts
+++ b/web-console/src/editor-completions/sql-completions.spec.ts
@@ -38,6 +38,13 @@ describe('sql-completions', () => {
return getSqlCompletions(context, word, options);
}
+ // The keywords suggested with the cursor at the |
+ function keywordsAt(textWithCursor: string): string[] {
+ return completionsAt(textWithCursor)
+ .filter(c => c.detail === 'keyword')
+ .map(c => c.label);
+ }
+
const columnMetadata: ColumnMetadata[] = [
{
TABLE_SCHEMA: 'druid',
@@ -130,6 +137,54 @@ describe('sql-completions', () => {
expect(keywordValues).not.toContain('SELECT');
});
+ it('suggests NULLS FIRST / NULLS LAST after an ORDER BY direction', () => {
+ expect(keywordsAt('SELECT * FROM t ORDER BY 1 DESC
|')).toEqual(['NULLS']);
+ expect(keywordsAt('SELECT * FROM t ORDER BY 1 ASC
|')).toEqual(['NULLS']);
+ expect(keywordsAt('SELECT * FROM t ORDER BY 1 DESC NULLS
|')).toEqual(['FIRST', 'LAST']);
+ expect(keywordsAt('SELECT * FROM t ORDER BY 1 DESC NULLS LAST
|')).toEqual([]);
+ expect(keywordsAt('SELECT * FROM t ORDER BY 1 DESC NULLS FIRST
|')).toEqual([]);
+ expect(keywordsAt('SELECT * FROM t ORDER BY 1 nulls\n first
|')).toEqual([]);
+
+ // FIRST is still followed by ROW / ROWS in a FETCH
+ expect(keywordsAt('SELECT * FROM t ORDER BY 1 FETCH FIRST
|')).toEqual(['ROW', 'ROWS']);
+ });
+
+ it('suggests keywords based on the two keywords before the cursor', () => {
+ // BY
+ expect(keywordsAt('SELECT * FROM t GROUP BY |')).toEqual(['GROUPING',
'ROLLUP', 'CUBE']);
+ expect(keywordsAt('REPLACE INTO t SELECT * FROM s PARTITIONED BY
|')).toEqual([
+ 'ALL',
+ 'HOUR',
+ 'DAY',
+ 'MONTH',
+ 'YEAR',
+ ]);
+ expect(keywordsAt('SELECT * FROM t ORDER BY |')).toEqual([]);
+ expect(keywordsAt('SELECT ROW_NUMBER() OVER (PARTITION BY
|')).toEqual([]);
+ expect(
+ keywordsAt('REPLACE INTO t SELECT * FROM s PARTITIONED BY DAY
CLUSTERED BY |'),
+ ).toEqual([]);
+
+ // ALL
+ expect(keywordsAt('REPLACE INTO t SELECT * FROM s PARTITIONED BY ALL
|')).toEqual(['TIME']);
+ expect(keywordsAt('SELECT ALL |')).toEqual([]);
+ expect(keywordsAt('SELECT 1 UNION ALL |')).toEqual(['SELECT']);
+
+ // DISTINCT
+ expect(keywordsAt('SELECT DISTINCT |')).toEqual([]);
+ expect(keywordsAt('SELECT COUNT(DISTINCT |')).toEqual([]);
+ expect(keywordsAt('SELECT * FROM t WHERE x IS |')).toEqual(['NOT',
'DISTINCT']);
+ expect(keywordsAt('SELECT * FROM t WHERE x IS NOT
|')).toEqual(['DISTINCT']);
+ expect(keywordsAt('SELECT * FROM t WHERE x IS DISTINCT
|')).toEqual(['FROM']);
+ expect(keywordsAt('SELECT * FROM t WHERE x IS NOT DISTINCT
|')).toEqual(['FROM']);
+
+ // Others
+ expect(keywordsAt('SELECT * FROM t LEFT OUTER |')).toEqual(['JOIN']);
+ expect(keywordsAt('SELECT SUM(x) OVER (ORDER BY y ROWS CURRENT ROW
|')).toEqual([]);
+ expect(keywordsAt('SELECT * FROM t FETCH FIRST ROW
|')).toEqual(['ONLY']);
+ expect(keywordsAt('EXPLAIN PLAN FOR |')).toEqual(['SELECT', 'INSERT',
'REPLACE']);
+ });
+
it('looks at the keyword on an earlier line and past comments', () => {
for (const sql of ['SELECT * FROM t GROUP\n |', 'SELECT * FROM t GROUP
/* by what */ |']) {
expect(
diff --git a/web-console/src/editor-completions/sql-completions.ts
b/web-console/src/editor-completions/sql-completions.ts
index 8ce3859fb83..5402352dce0 100644
--- a/web-console/src/editor-completions/sql-completions.ts
+++ b/web-console/src/editor-completions/sql-completions.ts
@@ -42,7 +42,9 @@ const SQL_KEYWORDS_THAT_CAN_NOT_BE_FOLLOWED_BY_FUNCTION = [
'SET',
'VALUES',
'FETCH',
+ 'NULLS',
'FIRST',
+ 'LAST',
'NEXT',
'ONLY',
'PRECEDING',
@@ -55,7 +57,9 @@ const SQL_KEYWORDS_THAT_CAN_NOT_BE_FOLLOWED_BY_REF = [
'DESC',
'LIMIT',
'OFFSET',
+ 'NULLS',
'FIRST',
+ 'LAST',
'NEXT',
'ONLY',
'PRECEDING',
@@ -93,6 +97,7 @@ const SQL_KEYWORD_FOLLOW_SUGGESTIONS: Record<string,
string[]> = {
RIGHT: ['JOIN', 'OUTER'],
INNER: ['JOIN'],
FULL: ['JOIN', 'OUTER'],
+ OUTER: ['JOIN'],
CROSS: ['JOIN'],
NATURAL: ['JOIN', 'LEFT', 'RIGHT', 'INNER', 'FULL'],
EXPLAIN: ['PLAN'],
@@ -100,18 +105,20 @@ const SQL_KEYWORD_FOLLOW_SUGGESTIONS: Record<string,
string[]> = {
GROUPING: ['SETS'],
FETCH: ['FIRST', 'NEXT'],
FIRST: ['ROW', 'ROWS'],
+ ASC: ['NULLS'],
+ DESC: ['NULLS'],
+ NULLS: ['FIRST', 'LAST'],
NEXT: ['ROW', 'ROWS'],
ROWS: ['ONLY'],
ROW: ['ONLY'],
IS: ['NOT', 'DISTINCT'],
- DISTINCT: ['FROM'],
BY: ['GROUPING', 'ROLLUP', 'CUBE', 'ALL', 'HOUR', 'DAY', 'MONTH', 'YEAR'],
ALL: ['TIME'],
// Keywords that cannot be followed by any other keyword
AS: [],
- ASC: [],
- DESC: [],
+ DISTINCT: [], // Except in IS [NOT] DISTINCT FROM, see
SQL_KEYWORD_PAIR_FOLLOW_SUGGESTIONS
+ LAST: [],
LIMIT: [],
OFFSET: [],
RETURNING: [],
@@ -157,6 +164,29 @@ const SQL_KEYWORD_FOLLOW_SUGGESTIONS: Record<string,
string[]> = {
MILLENNIUM: [],
};
+// Like SQL_KEYWORD_FOLLOW_SUGGESTIONS but keyed by the two keywords before
the cursor, takes precedence over it. For
+// keywords that are followed by different keywords depending on what comes
before them
+const SQL_KEYWORD_PAIR_FOLLOW_SUGGESTIONS: Record<string, string[]> = {
+ // BY on its own suggests the union of these
+ 'GROUP BY': ['GROUPING', 'ROLLUP', 'CUBE'],
+ 'PARTITIONED BY': ['ALL', 'HOUR', 'DAY', 'MONTH', 'YEAR'],
+ 'ORDER BY': [],
+ 'PARTITION BY': [],
+ 'CLUSTERED BY': [],
+
+ // ALL on its own is PARTITIONED BY ALL TIME
+ 'SELECT ALL': [],
+ 'UNION ALL': ['SELECT'],
+
+ 'IS NOT': ['DISTINCT'],
+ 'IS DISTINCT': ['FROM'],
+ 'NOT DISTINCT': ['FROM'],
+
+ 'NULLS FIRST': [], // Unlike FETCH FIRST
+ 'CURRENT ROW': [], // Unlike FETCH FIRST ROW
+ 'PLAN FOR': ['SELECT', 'INSERT', 'REPLACE'], // Unlike SUBSTRING(... FOR ...)
+};
+
const COMMENT_NODES = new Set(['LineComment', 'BlockComment', 'Issue']);
// The tokens that are words, the specializer in dsql-tokens.ts tells them
apart
@@ -233,6 +263,14 @@ export function getSqlCompletions(
tokenBeforePrefix && WORD_NODES.has(tokenBeforePrefix.name)
? state.sliceDoc(tokenBeforePrefix.from,
tokenBeforePrefix.to).toUpperCase()
: undefined;
+ const tokenBeforeKeyword =
+ tokenBeforePrefix && keywordBeforePrefix
+ ? tokenEndingBefore(state, tokenBeforePrefix.from)
+ : undefined;
+ const keywordsBeforePrefix =
+ tokenBeforeKeyword && WORD_NODES.has(tokenBeforeKeyword.name)
+ ? `${state.sliceDoc(tokenBeforeKeyword.from,
tokenBeforeKeyword.to).toUpperCase()} ${keywordBeforePrefix}`
+ : undefined;
// Other than literals, do not autocomplete numbers
if (/^\d+$/.test(prefix)) {
@@ -249,7 +287,11 @@ export function getSqlCompletions(
if (!quote) {
completions = completions.concat(
- (SQL_KEYWORD_FOLLOW_SUGGESTIONS[keywordBeforePrefix || ''] ||
SQL_KEYWORDS).map(v => ({
+ (
+ SQL_KEYWORD_PAIR_FOLLOW_SUGGESTIONS[keywordsBeforePrefix || ''] ||
+ SQL_KEYWORD_FOLLOW_SUGGESTIONS[keywordBeforePrefix || ''] ||
+ SQL_KEYWORDS
+ ).map(v => ({
label: v,
boost: 10,
detail: 'keyword',
diff --git a/web-console/src/editor-languages/dsql-keywords.ts
b/web-console/src/editor-languages/dsql-keywords.ts
index c7f94817a59..5cbea914c31 100644
--- a/web-console/src/editor-languages/dsql-keywords.ts
+++ b/web-console/src/editor-languages/dsql-keywords.ts
@@ -66,6 +66,7 @@ export const SQL_KEYWORDS = [
'ISOYEAR',
'JOIN',
'KEY',
+ 'LAST',
'LEADING',
'LEFT',
'LIKE',
@@ -81,6 +82,7 @@ export const SQL_KEYWORDS = [
'NATURAL',
'NEXT',
'NOT',
+ 'NULLS',
'OFFSET',
'ON',
'ONLY',
diff --git a/web-console/src/editor-languages/editor-languages.spec.ts
b/web-console/src/editor-languages/editor-languages.spec.ts
index b80064e3e8e..726b324324b 100644
--- a/web-console/src/editor-languages/editor-languages.spec.ts
+++ b/web-console/src/editor-languages/editor-languages.spec.ts
@@ -66,6 +66,21 @@ line */ SELECT`;
expect(tokenOf(dsqlLanguage, sql, '(x AS VARCHAR)')).toEqual('Parens');
});
+ it('highlights NULLS FIRST / NULLS LAST as keywords', () => {
+ expect(tokenize(dsqlLanguage, 'ORDER BY 2 DESC NULLS LAST, 1 NULLS
FIRST')).toEqual([
+ ['Keyword', 'ORDER'],
+ ['Keyword', 'BY'],
+ ['Number', '2'],
+ ['Keyword', 'DESC'],
+ ['Keyword', 'NULLS'],
+ ['Keyword', 'LAST'],
+ ['Punctuation', ','],
+ ['Number', '1'],
+ ['Keyword', 'NULLS'],
+ ['Keyword', 'FIRST'],
+ ]);
+ });
+
it('highlights unterminated DruidSQL tokens', () => {
expect(tokenOf(dsqlLanguage, 'SELECT /* not closed', '/* not
closed')).toEqual('BlockComment');
// An unterminated literal or quoted identifier lasts until the end of the
line
diff --git
a/web-console/src/views/explore-view/modules/grouping-table-module/grouping-table-module.tsx
b/web-console/src/views/explore-view/modules/grouping-table-module/grouping-table-module.tsx
index 937128454c7..0d9ddabb803 100644
---
a/web-console/src/views/explore-view/modules/grouping-table-module/grouping-table-module.tsx
+++
b/web-console/src/views/explore-view/modules/grouping-table-module/grouping-table-module.tsx
@@ -20,7 +20,7 @@ import { Button } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import type { Timezone } from 'chronoshift';
import type { SqlExpression, SqlOrderByDirection } from 'druid-query-toolkit';
-import { C, F } from 'druid-query-toolkit';
+import { C, F, SqlQuery } from 'druid-query-toolkit';
import { useMemo } from 'react';
import { Loader } from '../../../../components';
@@ -279,7 +279,7 @@
ModuleRepository.registerModule<GroupingTableParameterValues>({
const { query, columnHints } = queryAndHints;
let result = await runSqlQuery({ query, timezone }, signal);
- if (result.sqlQuery) {
+ if (result.sqlQuery instanceof SqlQuery) {
result = result.attachQuery(
{ query: '' },
result.sqlQuery.changeWhereExpression(globalWhere),
diff --git
a/web-console/src/views/sql-data-loader-view/schema-step/preview-table/preview-table.tsx
b/web-console/src/views/sql-data-loader-view/schema-step/preview-table/preview-table.tsx
index 9fafa1bf916..1761c9274d4 100644
---
a/web-console/src/views/sql-data-loader-view/schema-step/preview-table/preview-table.tsx
+++
b/web-console/src/views/sql-data-loader-view/schema-step/preview-table/preview-table.tsx
@@ -19,7 +19,7 @@
import { Icon, PopoverNext } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import classNames from 'classnames';
-import type { Column, QueryResult, SqlExpression, SqlQuery } from
'druid-query-toolkit';
+import type { Column, QueryResult, SqlExpression, SqlQueryBase } from
'druid-query-toolkit';
import { SqlAlias, SqlFunction, SqlStar } from 'druid-query-toolkit';
import React, { useState } from 'react';
@@ -48,7 +48,7 @@ function isWrappedInArrayToMv(ex: SqlExpression | undefined) {
return ex instanceof SqlFunction && ex.getEffectiveFunctionName() ===
'ARRAY_TO_MV';
}
-function formatFormulaAtIndex(query: SqlQuery, selectIndex: number): string {
+function formatFormulaAtIndex(query: SqlQueryBase, selectIndex: number):
string {
const ex = query.getSelectExpressionForIndex(selectIndex);
if (query.isRealOutputColumnAtSelectIndex(selectIndex)) {
diff --git
a/web-console/src/views/workbench-view/flexible-query-input/sub-query-markers.spec.ts
b/web-console/src/views/workbench-view/flexible-query-input/sub-query-markers.spec.ts
index db70d04070b..9dba7d51cb9 100644
---
a/web-console/src/views/workbench-view/flexible-query-input/sub-query-markers.spec.ts
+++
b/web-console/src/views/workbench-view/flexible-query-input/sub-query-markers.spec.ts
@@ -70,6 +70,28 @@ describe('sub-query-markers', () => {
view.destroy();
});
+ it('leaves just the line number when a run button goes away', () => {
+ const query = 'SELECT 1\nFROM t\nORDER BY 1 DESC ';
+ const view = new EditorView({
+ state: State.create({ doc: query, extensions: subQueryMarkers(() => {})
}),
+ });
+ const firstLineNumber = () =>
+ view.dom.querySelectorAll('.cm-lineNumbers
.cm-gutterElement')[1]?.textContent;
+
+ // Typing a stray word makes the query only parsable as a sub query (so it
gets a run button), deleting it puts
+ // the text back to a whole query (so the button goes away)
+ for (let i = 0; i < 3; i++) {
+ view.dispatch({ changes: { from: query.length, insert: 'N' } });
+
expect(view.dom.querySelectorAll('.sub-query-run-button')).toHaveLength(1);
+ expect(firstLineNumber()).toEqual('1');
+
+ view.dispatch({ changes: { from: query.length, to: query.length + 1 } });
+
expect(view.dom.querySelectorAll('.sub-query-run-button')).toHaveLength(0);
+ expect(firstLineNumber()).toEqual('1');
+ }
+ view.destroy();
+ });
+
it('has no queries without the extension', () => {
expect(getSubQueries(State.create({ doc: TEXT }))).toEqual([]);
});
diff --git
a/web-console/src/views/workbench-view/flexible-query-input/sub-query-markers.ts
b/web-console/src/views/workbench-view/flexible-query-input/sub-query-markers.ts
index 04a4cba8b2d..d2622262b48 100644
---
a/web-console/src/views/workbench-view/flexible-query-input/sub-query-markers.ts
+++
b/web-console/src/views/workbench-view/flexible-query-input/sub-query-markers.ts
@@ -75,9 +75,11 @@ class SubQueryMarker extends GutterMarker {
button.setAttribute('data-tooltip', 'Run this query');
button.append(icon);
- const fragment = document.createDocumentFragment();
- fragment.append(button, String(this.lineNumber));
- return fragment;
+ // The gutter expects a single node per marker (it removes just the one
node when the marker goes away) so the button
+ // and the line number must be wrapped together
+ const wrapper = document.createElement('span');
+ wrapper.append(button, String(this.lineNumber));
+ return wrapper;
}
}
diff --git
a/web-console/src/views/workbench-view/result-table-pane/result-table-pane.tsx
b/web-console/src/views/workbench-view/result-table-pane/result-table-pane.tsx
index 01e389ed6b1..83b5340e2fe 100644
---
a/web-console/src/views/workbench-view/result-table-pane/result-table-pane.tsx
+++
b/web-console/src/views/workbench-view/result-table-pane/result-table-pane.tsx
@@ -19,7 +19,7 @@
import { Button, Icon, Intent, Menu, MenuItem, PopoverNext } from
'@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import classNames from 'classnames';
-import type { Column, QueryResult, SqlExpression, SqlQuery } from
'druid-query-toolkit';
+import type { Column, QueryResult, SqlExpression, SqlQueryBase } from
'druid-query-toolkit';
import { C, F, SqlAlias, SqlFunction, SqlLiteral, SqlStar, SqlType } from
'druid-query-toolkit';
import * as JSONBig from 'json-bigint-native';
import type { JSX } from 'react';
@@ -91,7 +91,11 @@ function isStringJsonString(value: string): boolean {
}
}
-function getExpressionIfAlias(query: SqlQuery, columnIndex: number,
numColumns: number): string {
+function getExpressionIfAlias(
+ query: SqlQueryBase,
+ columnIndex: number,
+ numColumns: number,
+): string {
const selectExpressionsArray = query.getSelectExpressionsArray();
// If there is a * before
if (
diff --git a/web-console/tsconfig.json b/web-console/tsconfig.json
index ca1c7539b62..1bf71a21281 100644
--- a/web-console/tsconfig.json
+++ b/web-console/tsconfig.json
@@ -19,7 +19,7 @@
"strict": true,
"useUnknownInCatchVariables": false,
"target": "es2022",
- "types": ["jest"]
+ "types": ["jest", "node"]
},
"include": ["src", "e2e-tests", "typings"]
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]