You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@druid.apache.org by vo...@apache.org on 2020/04/17 00:52:12 UTC

[druid] branch master updated: add joins to column tree menu (#9705)

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 b7fdb29  add joins to column tree menu (#9705)
b7fdb29 is described below

commit b7fdb294237eccc39149da3fd7751ddd31e2fc2d
Author: mcbrewster <37...@users.noreply.github.com>
AuthorDate: Thu Apr 16 18:51:59 2020 -0600

    add joins to column tree menu (#9705)
    
    * add joins to column tree menu
    
    * fix capitalization
    
    * add keyword, keep columns if replaced
    
    * actually fix capitalization
    
    * add keywords
---
 web-console/lib/keywords.js                        |   7 ++
 web-console/package-lock.json                      |   6 +-
 web-console/package.json                           |   2 +-
 .../number-menu-items/number-menu-items.spec.tsx   |   4 +
 .../number-menu-items/number-menu-items.tsx        |  94 ++++++++++++++++--
 .../string-menu-items/string-menu-items.spec.tsx   |   4 +
 .../string-menu-items/string-menu-items.tsx        |  81 +++++++++++++++-
 .../time-menu-items/time-menu-items.spec.tsx       |   4 +
 .../time-menu-items/time-menu-items.tsx            | 105 +++++++++++++++++----
 .../views/query-view/column-tree/column-tree.tsx   | 101 +++++++++++++++++++-
 10 files changed, 373 insertions(+), 35 deletions(-)

diff --git a/web-console/lib/keywords.js b/web-console/lib/keywords.js
index c7261cd..a0ec6fe 100644
--- a/web-console/lib/keywords.js
+++ b/web-console/lib/keywords.js
@@ -34,6 +34,13 @@ exports.SQL_KEYWORDS = [
   'DESC',
   'LIMIT',
   'UNION ALL',
+  'JOIN',
+  'LEFT',
+  'INNER',
+  'ON',
+  'RIGHT',
+  'OUTER',
+  'FULL'
 ];
 
 exports.SQL_EXPRESSION_PARTS = [
diff --git a/web-console/package-lock.json b/web-console/package-lock.json
index 534b6dd..94954ef 100644
--- a/web-console/package-lock.json
+++ b/web-console/package-lock.json
@@ -4243,9 +4243,9 @@
       }
     },
     "druid-query-toolkit": {
-      "version": "0.4.3",
-      "resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.4.3.tgz",
-      "integrity": "sha512-g2xHaD9lhpTZjM9UEiOQBpgBvO1hm0/cVgqymbUIUf6cgBCm7C1o20fHQzCEK4FBhVmWQSt63pjhRaQ+OM4FGQ==",
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.5.1.tgz",
+      "integrity": "sha512-oI1YddnzIbkcelI93qaRtonu3PLGw65fDqLLK6e35gD4Ef/Yf8bOZvFK9wDYNAXcA6SDy7UDarfWsgD2dDWsjg==",
       "requires": {
         "tslib": "^1.10.0"
       }
diff --git a/web-console/package.json b/web-console/package.json
index 105fddc..4bf2d91 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -68,7 +68,7 @@
     "d3-axis": "^1.0.12",
     "d3-scale": "^3.2.0",
     "d3-selection": "^1.4.0",
-    "druid-query-toolkit": "^0.4.3",
+    "druid-query-toolkit": "^0.5.1",
     "file-saver": "^2.0.2",
     "has-own-prop": "^2.0.0",
     "hjson": "^3.2.1",
diff --git a/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.spec.tsx b/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.spec.tsx
index 3ca046e..491ecdd 100644
--- a/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.spec.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.spec.tsx
@@ -28,6 +28,8 @@ describe('number menu', () => {
   it('matches snapshot when menu is opened for column not inside group by', () => {
     const numberMenu = (
       <NumberMenuItems
+        schema="schema"
+        table="table"
         columnName={'added'}
         parsedQuery={parser(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`)}
         onQueryChange={() => {}}
@@ -41,6 +43,8 @@ describe('number menu', () => {
   it('matches snapshot when menu is opened for column inside group by', () => {
     const numberMenu = (
       <NumberMenuItems
+        schema="schema"
+        table="table"
         columnName={'added'}
         parsedQuery={parser(`SELECT added, count(*) as cnt FROM wikipedia GROUP BY 1`)}
         onQueryChange={() => {}}
diff --git a/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.tsx b/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.tsx
index db54853..ca8dc28 100644
--- a/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.tsx
@@ -18,10 +18,21 @@
 
 import { MenuItem } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
-import { SqlAliasRef, SqlFunction, SqlLiteral, SqlQuery, SqlRef } from 'druid-query-toolkit';
+import {
+  SqlAliasRef,
+  SqlFunction,
+  SqlLiteral,
+  SqlMulti,
+  SqlQuery,
+  SqlRef,
+} from 'druid-query-toolkit';
 import React from 'react';
 
+import { getCurrentColumns } from '../../column-tree';
+
 export interface NumberMenuItemsProps {
+  table: string;
+  schema: string;
   columnName: string;
   parsedQuery: SqlQuery;
   onQueryChange: (queryString: SqlQuery, run?: boolean) => void;
@@ -38,7 +49,7 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number
           onClick={() => {
             onQueryChange(
               parsedQuery.addWhereFilter(
-                SqlRef.fromNameWithDoubleQuotes(columnName),
+                SqlRef.fromStringWithDoubleQuotes(columnName),
                 '>',
                 SqlLiteral.fromInput(100),
               ),
@@ -50,7 +61,7 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number
           onClick={() => {
             onQueryChange(
               parsedQuery.addWhereFilter(
-                SqlRef.fromNameWithDoubleQuotes(columnName),
+                SqlRef.fromStringWithDoubleQuotes(columnName),
                 '<=',
                 SqlLiteral.fromInput(100),
               ),
@@ -100,7 +111,7 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number
           text={`"${columnName}"`}
           onClick={() => {
             onQueryChange(
-              parsedQuery.addToGroupBy(SqlRef.fromNameWithDoubleQuotes(columnName)),
+              parsedQuery.addToGroupBy(SqlRef.fromStringWithDoubleQuotes(columnName)),
               true,
             );
           }}
@@ -112,7 +123,7 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number
               parsedQuery.addToGroupBy(
                 SqlAliasRef.sqlAliasFactory(
                   SqlFunction.sqlFunctionFactory('TRUNC', [
-                    SqlRef.fromNameWithDoubleQuotes(columnName),
+                    SqlRef.fromStringWithDoubleQuotes(columnName),
                     SqlLiteral.fromInput(-1),
                   ]),
                   `${columnName}_truncated`,
@@ -137,7 +148,7 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number
           onClick={() => {
             onQueryChange(
               parsedQuery.addAggregateColumn(
-                [SqlRef.fromName(columnName)],
+                [SqlRef.fromString(columnName)],
                 'SUM',
                 `sum_${columnName}`,
               ),
@@ -150,7 +161,7 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number
           onClick={() => {
             onQueryChange(
               parsedQuery.addAggregateColumn(
-                [SqlRef.fromName(columnName)],
+                [SqlRef.fromString(columnName)],
                 'MAX',
                 `max_${columnName}`,
               ),
@@ -163,7 +174,7 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number
           onClick={() => {
             onQueryChange(
               parsedQuery.addAggregateColumn(
-                [SqlRef.fromName(columnName)],
+                [SqlRef.fromString(columnName)],
                 'MIN',
                 `min_${columnName}`,
               ),
@@ -175,6 +186,72 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number
     );
   }
 
+  function renderJoinMenu(): JSX.Element | undefined {
+    const { schema, table, columnName, parsedQuery, onQueryChange } = props;
+    if (schema !== 'lookup' || !parsedQuery) return;
+
+    const { originalTableColumn, lookupColumn } = getCurrentColumns(parsedQuery, table);
+
+    return (
+      <>
+        <MenuItem
+          icon={IconNames.JOIN_TABLE}
+          text={parsedQuery.joinTable ? `Replace join` : `Join`}
+        >
+          <MenuItem
+            icon={IconNames.LEFT_JOIN}
+            text={`Left join`}
+            onClick={() => {
+              onQueryChange(
+                parsedQuery.addJoin(
+                  'LEFT',
+                  SqlRef.fromString(table, schema).upgrade(),
+                  SqlMulti.sqlMultiFactory('=', [
+                    SqlRef.fromString(columnName, table, 'lookup'),
+                    SqlRef.fromString(
+                      lookupColumn === columnName ? originalTableColumn : 'XXX',
+                      parsedQuery.getTableName(),
+                    ),
+                  ]),
+                ),
+                false,
+              );
+            }}
+          />
+          <MenuItem
+            icon={IconNames.INNER_JOIN}
+            text={`Inner join`}
+            onClick={() => {
+              onQueryChange(
+                parsedQuery.addJoin(
+                  'INNER',
+                  SqlRef.fromString(table, schema).upgrade(),
+                  SqlMulti.sqlMultiFactory('=', [
+                    SqlRef.fromString(columnName, table, 'lookup'),
+                    SqlRef.fromString(
+                      lookupColumn === columnName ? originalTableColumn : 'XXX',
+                      parsedQuery.getTableName(),
+                    ),
+                  ]),
+                ),
+                false,
+              );
+            }}
+          />
+        </MenuItem>
+        {parsedQuery.onExpression &&
+          parsedQuery.onExpression instanceof SqlMulti &&
+          parsedQuery.onExpression.containsColumn(columnName) && (
+            <MenuItem
+              icon={IconNames.EXCHANGE}
+              text={`Remove join`}
+              onClick={() => onQueryChange(parsedQuery.removeJoin())}
+            />
+          )}
+      </>
+    );
+  }
+
   return (
     <>
       {renderFilterMenu()}
@@ -182,6 +259,7 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number
       {renderGroupByMenu()}
       {renderRemoveGroupBy()}
       {renderAggregateMenu()}
+      {renderJoinMenu()}
     </>
   );
 });
diff --git a/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.spec.tsx b/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.spec.tsx
index e952810..3f3c54f 100644
--- a/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.spec.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.spec.tsx
@@ -28,6 +28,8 @@ describe('string menu', () => {
   it('matches snapshot when menu is opened for column not inside group by', () => {
     const stringMenu = (
       <StringMenuItems
+        table={'table'}
+        schema={'schema'}
         columnName={'cityName'}
         parsedQuery={parser(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`)}
         onQueryChange={() => {}}
@@ -41,6 +43,8 @@ describe('string menu', () => {
   it('matches snapshot when menu is opened for column inside group by', () => {
     const stringMenu = (
       <StringMenuItems
+        table={'table'}
+        schema={'schema'}
         columnName={'channel'}
         parsedQuery={parser(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`)}
         onQueryChange={() => {}}
diff --git a/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx b/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx
index 25b8233..dcba6f0 100644
--- a/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx
@@ -28,7 +28,11 @@ import {
 } from 'druid-query-toolkit';
 import React from 'react';
 
+import { getCurrentColumns } from '../../column-tree';
+
 export interface StringMenuItemsProps {
+  schema: string;
+  table: string;
   columnName: string;
   parsedQuery: SqlQuery;
   onQueryChange: (queryString: SqlQuery, run?: boolean) => void;
@@ -95,7 +99,7 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String
           text={`"${columnName}"`}
           onClick={() => {
             onQueryChange(
-              parsedQuery.addToGroupBy(SqlRef.fromNameWithDoubleQuotes(columnName)),
+              parsedQuery.addToGroupBy(SqlRef.fromStringWithDoubleQuotes(columnName)),
               true,
             );
           }}
@@ -107,7 +111,7 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String
               parsedQuery.addToGroupBy(
                 SqlAliasRef.sqlAliasFactory(
                   SqlFunction.sqlFunctionFactory('SUBSTRING', [
-                    SqlRef.fromNameWithDoubleQuotes(columnName),
+                    SqlRef.fromStringWithDoubleQuotes(columnName),
                     SqlLiteral.fromInput(1),
                     SqlLiteral.fromInput(2),
                   ]),
@@ -133,7 +137,7 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String
           onClick={() =>
             onQueryChange(
               parsedQuery.addAggregateColumn(
-                [SqlRef.fromNameWithDoubleQuotes(columnName)],
+                [SqlRef.fromStringWithDoubleQuotes(columnName)],
                 'COUNT',
                 `dist_${columnName}`,
                 undefined,
@@ -148,11 +152,11 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String
           onClick={() => {
             onQueryChange(
               parsedQuery.addAggregateColumn(
-                [SqlRef.fromName('*')],
+                [SqlRef.fromString('*')],
                 'COUNT',
                 `${columnName}_filtered_count`,
                 SqlMulti.sqlMultiFactory('=', [
-                  SqlRef.fromNameWithDoubleQuotes(columnName),
+                  SqlRef.fromStringWithDoubleQuotes(columnName),
                   SqlLiteral.fromInput('xxx'),
                 ]),
               ),
@@ -163,6 +167,72 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String
     );
   }
 
+  function renderJoinMenu(): JSX.Element | undefined {
+    const { schema, table, columnName, parsedQuery, onQueryChange } = props;
+    if (schema !== 'lookup' || !parsedQuery) return;
+
+    const { originalTableColumn, lookupColumn } = getCurrentColumns(parsedQuery, table);
+
+    return (
+      <>
+        <MenuItem
+          icon={IconNames.JOIN_TABLE}
+          text={parsedQuery.joinTable ? `Replace join` : `Join`}
+        >
+          <MenuItem
+            icon={IconNames.LEFT_JOIN}
+            text={`Left join`}
+            onClick={() => {
+              onQueryChange(
+                parsedQuery.addJoin(
+                  'LEFT',
+                  SqlRef.fromString(table, schema).upgrade(),
+                  SqlMulti.sqlMultiFactory('=', [
+                    SqlRef.fromString(columnName, table, 'lookup'),
+                    SqlRef.fromString(
+                      lookupColumn === columnName ? originalTableColumn : 'XXX',
+                      parsedQuery.getTableName(),
+                    ),
+                  ]),
+                ),
+                false,
+              );
+            }}
+          />
+          <MenuItem
+            icon={IconNames.INNER_JOIN}
+            text={`Inner join`}
+            onClick={() => {
+              onQueryChange(
+                parsedQuery.addJoin(
+                  'INNER',
+                  SqlRef.fromString(table, schema).upgrade(),
+                  SqlMulti.sqlMultiFactory('=', [
+                    SqlRef.fromString(columnName, table, 'lookup'),
+                    SqlRef.fromString(
+                      lookupColumn === columnName ? originalTableColumn : 'XXX',
+                      parsedQuery.getTableName(),
+                    ),
+                  ]),
+                ),
+                false,
+              );
+            }}
+          />
+        </MenuItem>
+        {parsedQuery.onExpression &&
+          parsedQuery.onExpression instanceof SqlMulti &&
+          parsedQuery.onExpression.containsColumn(columnName) && (
+            <MenuItem
+              icon={IconNames.EXCHANGE}
+              text={`Remove join`}
+              onClick={() => onQueryChange(parsedQuery.removeJoin())}
+            />
+          )}
+      </>
+    );
+  }
+
   return (
     <>
       {renderFilterMenu()}
@@ -170,6 +240,7 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String
       {renderGroupByMenu()}
       {renderRemoveGroupBy()}
       {renderAggregateMenu()}
+      {renderJoinMenu()}
     </>
   );
 });
diff --git a/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.spec.tsx b/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.spec.tsx
index a6a1c9a..a8d5ee7 100644
--- a/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.spec.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.spec.tsx
@@ -28,6 +28,8 @@ describe('time menu', () => {
   it('matches snapshot when menu is opened for column not inside group by', () => {
     const timeMenu = (
       <TimeMenuItems
+        table={'table'}
+        schema={'schema'}
         columnName={'__time'}
         parsedQuery={parser(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`)}
         onQueryChange={() => {}}
@@ -41,6 +43,8 @@ describe('time menu', () => {
   it('matches snapshot when menu is opened for column inside group by', () => {
     const timeMenu = (
       <TimeMenuItems
+        table={'table'}
+        schema={'schema'}
         columnName={'__time'}
         parsedQuery={parser(`SELECT __time, count(*) as cnt FROM wikipedia GROUP BY 1`)}
         onQueryChange={() => {}}
diff --git a/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx b/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx
index 310fea8..e2e7e18 100644
--- a/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx
@@ -30,6 +30,8 @@ import {
 } from 'druid-query-toolkit';
 import React from 'react';
 
+import { getCurrentColumns } from '../../column-tree';
+
 function dateToTimestamp(date: Date): SqlTimestamp {
   return SqlTimestamp.sqlTimestampFactory(
     date
@@ -91,6 +93,8 @@ function nextYear(dt: Date): Date {
 }
 
 export interface TimeMenuItemsProps {
+  table: string;
+  schema: string;
   columnName: string;
   parsedQuery: SqlQuery;
   onQueryChange: (queryString: SqlQuery, run?: boolean) => void;
@@ -113,7 +117,7 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
                   columnName,
                   '>=',
                   SqlMulti.sqlMultiFactory('-', [
-                    SqlRef.fromName('CURRENT_TIMESTAMP'),
+                    SqlRef.fromString('CURRENT_TIMESTAMP'),
                     SqlInterval.sqlIntervalFactory('HOUR', 1),
                   ]),
                 ),
@@ -131,7 +135,7 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
                   columnName,
                   '>=',
                   SqlMulti.sqlMultiFactory('-', [
-                    SqlRef.fromName('CURRENT_TIMESTAMP'),
+                    SqlRef.fromString('CURRENT_TIMESTAMP'),
                     SqlInterval.sqlIntervalFactory('Day', 1),
                   ]),
                 ),
@@ -149,7 +153,7 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
                   columnName,
                   '>=',
                   SqlMulti.sqlMultiFactory('-', [
-                    SqlRef.fromName('CURRENT_TIMESTAMP'),
+                    SqlRef.fromString('CURRENT_TIMESTAMP'),
                     SqlInterval.sqlIntervalFactory('Day', 7),
                   ]),
                 ),
@@ -167,7 +171,7 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
                   columnName,
                   '>=',
                   SqlMulti.sqlMultiFactory('-', [
-                    SqlRef.fromName('CURRENT_TIMESTAMP'),
+                    SqlRef.fromString('CURRENT_TIMESTAMP'),
                     SqlInterval.sqlIntervalFactory('MONTH', 1),
                   ]),
                 ),
@@ -185,7 +189,7 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
                   columnName,
                   '>=',
                   SqlMulti.sqlMultiFactory('-', [
-                    SqlRef.fromName('CURRENT_TIMESTAMP'),
+                    SqlRef.fromString('CURRENT_TIMESTAMP'),
                     SqlInterval.sqlIntervalFactory('YEAR', 1),
                   ]),
                 ),
@@ -202,12 +206,12 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
               parsedQuery
                 .removeFilter(columnName)
                 .addWhereFilter(
-                  SqlRef.fromNameWithDoubleQuotes(columnName),
+                  SqlRef.fromStringWithDoubleQuotes(columnName),
                   '>=',
                   dateToTimestamp(hourStart),
                 )
                 .addWhereFilter(
-                  SqlRef.fromNameWithDoubleQuotes(columnName),
+                  SqlRef.fromStringWithDoubleQuotes(columnName),
                   '<',
                   dateToTimestamp(nextHour(hourStart)),
                 ),
@@ -223,12 +227,12 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
               parsedQuery
                 .removeFilter(columnName)
                 .addWhereFilter(
-                  SqlRef.fromNameWithDoubleQuotes(columnName),
+                  SqlRef.fromStringWithDoubleQuotes(columnName),
                   '>=',
                   dateToTimestamp(dayStart),
                 )
                 .addWhereFilter(
-                  SqlRef.fromNameWithDoubleQuotes(columnName),
+                  SqlRef.fromStringWithDoubleQuotes(columnName),
                   '<',
                   dateToTimestamp(nextDay(dayStart)),
                 ),
@@ -244,12 +248,12 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
               parsedQuery
                 .removeFilter(columnName)
                 .addWhereFilter(
-                  SqlRef.fromNameWithDoubleQuotes(columnName),
+                  SqlRef.fromStringWithDoubleQuotes(columnName),
                   '>=',
                   dateToTimestamp(monthStart),
                 )
                 .addWhereFilter(
-                  SqlRef.fromNameWithDoubleQuotes(columnName),
+                  SqlRef.fromStringWithDoubleQuotes(columnName),
                   '<',
                   dateToTimestamp(nextMonth(monthStart)),
                 ),
@@ -265,7 +269,7 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
               parsedQuery
                 .removeFilter(columnName)
                 .addWhereFilter(
-                  SqlRef.fromNameWithDoubleQuotes(columnName),
+                  SqlRef.fromStringWithDoubleQuotes(columnName),
                   '<=',
                   dateToTimestamp(yearStart),
                 )
@@ -324,7 +328,7 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
               parsedQuery.addToGroupBy(
                 SqlAliasRef.sqlAliasFactory(
                   SqlFunction.sqlFunctionFactory('TIME_FLOOR', [
-                    SqlRef.fromName(columnName),
+                    SqlRef.fromString(columnName),
                     SqlLiteral.fromInput('PT1h'),
                   ]),
                   `${columnName}_time_floor`,
@@ -341,7 +345,7 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
               parsedQuery.addToGroupBy(
                 SqlAliasRef.sqlAliasFactory(
                   SqlFunction.sqlFunctionFactory('TIME_FLOOR', [
-                    SqlRef.fromName(columnName),
+                    SqlRef.fromString(columnName),
                     SqlLiteral.fromInput('P1D'),
                   ]),
                   `${columnName}_time_floor`,
@@ -358,7 +362,7 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
               parsedQuery.addToGroupBy(
                 SqlAliasRef.sqlAliasFactory(
                   SqlFunction.sqlFunctionFactory('TIME_FLOOR', [
-                    SqlRef.fromName(columnName),
+                    SqlRef.fromString(columnName),
                     SqlLiteral.fromInput('P7D'),
                   ]),
                   `${columnName}_time_floor`,
@@ -383,7 +387,7 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
           onClick={() => {
             onQueryChange(
               parsedQuery.addAggregateColumn(
-                [SqlRef.fromNameWithDoubleQuotes(columnName)],
+                [SqlRef.fromStringWithDoubleQuotes(columnName)],
                 'MAX',
                 `max_${columnName}`,
               ),
@@ -396,7 +400,7 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
           onClick={() => {
             onQueryChange(
               parsedQuery.addAggregateColumn(
-                [SqlRef.fromNameWithDoubleQuotes(columnName)],
+                [SqlRef.fromStringWithDoubleQuotes(columnName)],
                 'MIN',
                 `min_${columnName}`,
               ),
@@ -408,6 +412,72 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
     );
   }
 
+  function renderJoinMenu(): JSX.Element | undefined {
+    const { schema, table, columnName, parsedQuery, onQueryChange } = props;
+    if (schema !== 'lookup' || !parsedQuery) return;
+
+    const { originalTableColumn, lookupColumn } = getCurrentColumns(parsedQuery, table);
+
+    return (
+      <>
+        <MenuItem
+          icon={IconNames.JOIN_TABLE}
+          text={parsedQuery.joinTable ? `Replace join` : `Join`}
+        >
+          <MenuItem
+            icon={IconNames.LEFT_JOIN}
+            text={`Left join`}
+            onClick={() => {
+              onQueryChange(
+                parsedQuery.addJoin(
+                  'LEFT',
+                  SqlRef.fromString(table, schema).upgrade(),
+                  SqlMulti.sqlMultiFactory('=', [
+                    SqlRef.fromString(columnName, table, 'lookup'),
+                    SqlRef.fromString(
+                      lookupColumn === columnName ? originalTableColumn : 'XXX',
+                      parsedQuery.getTableName(),
+                    ),
+                  ]),
+                ),
+                false,
+              );
+            }}
+          />
+          <MenuItem
+            icon={IconNames.INNER_JOIN}
+            text={`Inner join`}
+            onClick={() => {
+              onQueryChange(
+                parsedQuery.addJoin(
+                  'INNER',
+                  SqlRef.fromString(table, schema).upgrade(),
+                  SqlMulti.sqlMultiFactory('=', [
+                    SqlRef.fromString(columnName, table, 'lookup'),
+                    SqlRef.fromString(
+                      lookupColumn === columnName ? originalTableColumn : 'XXX',
+                      parsedQuery.getTableName(),
+                    ),
+                  ]),
+                ),
+                false,
+              );
+            }}
+          />
+        </MenuItem>
+        {parsedQuery.onExpression &&
+          parsedQuery.onExpression instanceof SqlMulti &&
+          parsedQuery.onExpression.containsColumn(columnName) && (
+            <MenuItem
+              icon={IconNames.EXCHANGE}
+              text={`Remove join`}
+              onClick={() => onQueryChange(parsedQuery.removeJoin())}
+            />
+          )}
+      </>
+    );
+  }
+
   return (
     <>
       {renderFilterMenu()}
@@ -415,6 +485,7 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
       {renderGroupByMenu()}
       {renderRemoveGroupBy()}
       {renderAggregateMenu()}
+      {renderJoinMenu()}
     </>
   );
 });
diff --git a/web-console/src/views/query-view/column-tree/column-tree.tsx b/web-console/src/views/query-view/column-tree/column-tree.tsx
index fd98ccf..d672b07 100644
--- a/web-console/src/views/query-view/column-tree/column-tree.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree.tsx
@@ -27,7 +27,7 @@ import {
   Tree,
 } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
-import { SqlQuery } from 'druid-query-toolkit';
+import { SqlMulti, SqlQuery, SqlRef } from 'druid-query-toolkit';
 import React, { ChangeEvent } from 'react';
 
 import { Loader } from '../../../components';
@@ -125,6 +125,32 @@ export interface ColumnTreeState {
   selectedTreeIndex: number;
 }
 
+export function getCurrentColumns(parsedQuery: SqlQuery, table: string) {
+  let lookupColumn;
+  let originalTableColumn;
+  if (
+    parsedQuery.joinTable &&
+    parsedQuery.joinTable.table === table &&
+    parsedQuery.onExpression &&
+    parsedQuery.onExpression instanceof SqlMulti
+  ) {
+    parsedQuery.onExpression.arguments.map(argument => {
+      if (argument instanceof SqlRef) {
+        if (argument.namespace === 'lookup') {
+          lookupColumn = argument.column;
+        } else {
+          originalTableColumn = argument.column;
+        }
+      }
+    });
+  }
+
+  return {
+    lookupColumn: lookupColumn || 'XXX',
+    originalTableColumn: originalTableColumn || 'XXX',
+  };
+}
+
 export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeState> {
   static getDerivedStateFromProps(props: ColumnTreeProps, state: ColumnTreeState) {
     const { columnMetadata, defaultSchema, defaultTable } = props;
@@ -188,6 +214,73 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
                                 }}
                               />
                             )}
+                            {parsedQuery && schema === 'lookup' && (
+                              <MenuItem
+                                popoverProps={{ openOnTargetFocus: false }}
+                                icon={IconNames.JOIN_TABLE}
+                                text={parsedQuery.joinTable ? `Replace join` : `Join`}
+                              >
+                                <MenuItem
+                                  icon={IconNames.LEFT_JOIN}
+                                  text={`Left join`}
+                                  onClick={() => {
+                                    const { lookupColumn, originalTableColumn } = getCurrentColumns(
+                                      parsedQuery,
+                                      table,
+                                    );
+                                    props.onQueryStringChange(
+                                      parsedQuery.addJoin(
+                                        'LEFT',
+                                        SqlRef.fromString(table, schema).upgrade(),
+                                        SqlMulti.sqlMultiFactory('=', [
+                                          SqlRef.fromString(lookupColumn, table, 'lookup'),
+                                          SqlRef.fromString(
+                                            originalTableColumn,
+                                            parsedQuery.getTableName(),
+                                          ),
+                                        ]),
+                                      ),
+                                      false,
+                                    );
+                                  }}
+                                />
+                                <MenuItem
+                                  icon={IconNames.INNER_JOIN}
+                                  text={`Inner join`}
+                                  onClick={() => {
+                                    const { lookupColumn, originalTableColumn } = getCurrentColumns(
+                                      parsedQuery,
+                                      table,
+                                    );
+                                    props.onQueryStringChange(
+                                      parsedQuery.addJoin(
+                                        'INNER',
+                                        SqlRef.fromString(table, schema).upgrade(),
+                                        SqlMulti.sqlMultiFactory('=', [
+                                          SqlRef.fromString(lookupColumn, table, 'lookup'),
+                                          SqlRef.fromString(
+                                            originalTableColumn,
+                                            parsedQuery.getTableName(),
+                                          ),
+                                        ]),
+                                      ),
+                                      false,
+                                    );
+                                  }}
+                                />
+                              </MenuItem>
+                            )}
+                            {parsedQuery &&
+                              parsedQuery.joinTable &&
+                              parsedQuery.joinTable.table === table && (
+                                <MenuItem
+                                  icon={IconNames.EXCHANGE}
+                                  text={`Remove join`}
+                                  onClick={() =>
+                                    props.onQueryStringChange(parsedQuery.removeJoin())
+                                  }
+                                />
+                              )}
                           </Menu>
                         );
                       }}
@@ -234,6 +327,8 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
                                     (columnData.DATA_TYPE === 'BIGINT' ||
                                       columnData.DATA_TYPE === 'FLOAT') && (
                                       <NumberMenuItems
+                                        table={table}
+                                        schema={schema}
                                         columnName={columnData.COLUMN_NAME}
                                         parsedQuery={parsedQuery}
                                         onQueryChange={props.onQueryStringChange}
@@ -241,6 +336,8 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
                                     )}
                                   {parsedQuery && columnData.DATA_TYPE === 'VARCHAR' && (
                                     <StringMenuItems
+                                      table={table}
+                                      schema={schema}
                                       columnName={columnData.COLUMN_NAME}
                                       parsedQuery={parsedQuery}
                                       onQueryChange={props.onQueryStringChange}
@@ -248,6 +345,8 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
                                   )}
                                   {parsedQuery && columnData.DATA_TYPE === 'TIMESTAMP' && (
                                     <TimeMenuItems
+                                      table={table}
+                                      schema={schema}
                                       columnName={columnData.COLUMN_NAME}
                                       parsedQuery={parsedQuery}
                                       onQueryChange={props.onQueryStringChange}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org