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 6d8799f  Update QueryView to use latest DruidQueryToolkit (#10201)
6d8799f is described below

commit 6d8799f2dfec72c234f408a3433cf93f69c99ddb
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Thu Jul 23 22:45:01 2020 -0700

    Update QueryView to use latest DruidQueryToolkit (#10201)
    
    * Update to latest DruidQueryToolkit
    
    * add THEN keyword
    
    * do not crash on invalid JSON
---
 licenses.yaml                                      |   2 +-
 web-console/lib/keywords.js                        |   6 +
 web-console/package-lock.json                      |   6 +-
 web-console/package.json                           |   2 +-
 web-console/script/create-sql-docs.js              |  10 +-
 web-console/src/utils/general.tsx                  |   4 -
 web-console/src/utils/index.tsx                    |   1 +
 web-console/src/utils/query-cursor.ts              |  64 +++
 .../src/views/datasource-view/datasource-view.tsx  |   4 +-
 .../__snapshots__/query-view.spec.tsx.snap         |   4 +-
 .../number-menu-items/number-menu-items.spec.tsx   |   6 +-
 .../number-menu-items/number-menu-items.tsx        | 243 +++--------
 .../string-menu-items/string-menu-items.spec.tsx   |   6 +-
 .../string-menu-items/string-menu-items.tsx        | 245 +++++------
 .../time-menu-items/time-menu-items.spec.tsx       |   6 +-
 .../time-menu-items/time-menu-items.tsx            | 470 ++++++---------------
 .../query-view/column-tree/column-tree.spec.tsx    |   6 +-
 .../views/query-view/column-tree/column-tree.tsx   | 346 ++++++++-------
 .../__snapshots__/query-extra-info.spec.tsx.snap   |   2 +-
 .../query-extra-info/query-extra-info.spec.tsx     |  11 +-
 .../query-extra-info/query-extra-info.tsx          |  50 +--
 .../views/query-view/query-input/query-input.tsx   |  16 +-
 .../__snapshots__/query-output.spec.tsx.snap       |  38 ++
 .../query-view/query-output/query-output.scss      |   6 +
 .../query-view/query-output/query-output.spec.tsx  |  15 +-
 .../views/query-view/query-output/query-output.tsx | 330 ++++++++-------
 web-console/src/views/query-view/query-view.tsx    | 209 ++++-----
 27 files changed, 970 insertions(+), 1138 deletions(-)

diff --git a/licenses.yaml b/licenses.yaml
index e73c484..a83f20d 100644
--- a/licenses.yaml
+++ b/licenses.yaml
@@ -4707,7 +4707,7 @@ license_category: binary
 module: web-console
 license_name: Apache License version 2.0
 copyright: Imply Data
-version: 0.6.1
+version: 0.8.4
 
 ---
 
diff --git a/web-console/lib/keywords.js b/web-console/lib/keywords.js
index 09f4b27..69a3f87 100644
--- a/web-console/lib/keywords.js
+++ b/web-console/lib/keywords.js
@@ -28,6 +28,9 @@ exports.SQL_KEYWORDS = [
   'FROM',
   'WHERE',
   'GROUP BY',
+  'CUBE',
+  'ROLLUP',
+  'GROUPING SETS',
   'HAVING',
   'ORDER BY',
   'ASC',
@@ -48,6 +51,7 @@ exports.SQL_EXPRESSION_PARTS = [
   'END',
   'ELSE',
   'WHEN',
+  'THEN',
   'CASE',
   'OR',
   'AND',
@@ -56,7 +60,9 @@ exports.SQL_EXPRESSION_PARTS = [
   'IS',
   'TO',
   'BETWEEN',
+  'SYMMETRIC',
   'LIKE',
+  'SIMILAR',
   'ESCAPE',
   'BOTH',
   'LEADING',
diff --git a/web-console/package-lock.json b/web-console/package-lock.json
index 52de297..2f8012b 100644
--- a/web-console/package-lock.json
+++ b/web-console/package-lock.json
@@ -4243,9 +4243,9 @@
       }
     },
     "druid-query-toolkit": {
-      "version": "0.6.1",
-      "resolved": 
"https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.6.1.tgz";,
-      "integrity": 
"sha512-ykrWD9AbDQEvE55x8ST1kyiiGHSN8zhp/Lqe7z43/l7XG9QD7AQwfBzOn+HATXFynrOQN/3z3Cis70EzdDjc1g==",
+      "version": "0.8.4",
+      "resolved": 
"https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.8.4.tgz";,
+      "integrity": 
"sha512-d0/OJDh6xNxlmqu874v1K2yGH0DD5ZXhRGR8iRFNDRUUZzTLSIdOTyaulHxCQ8j1bpfhB6VN+XTWzd0V6AgqbQ==",
       "requires": {
         "tslib": "^1.10.0"
       }
diff --git a/web-console/package.json b/web-console/package.json
index 3e517d4..da46a80 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.6.1",
+    "druid-query-toolkit": "^0.8.4",
     "file-saver": "^2.0.2",
     "has-own-prop": "^2.0.0",
     "hjson": "^3.2.1",
diff --git a/web-console/script/create-sql-docs.js 
b/web-console/script/create-sql-docs.js
index 092706d..ce09be3 100755
--- a/web-console/script/create-sql-docs.js
+++ b/web-console/script/create-sql-docs.js
@@ -23,6 +23,10 @@ const fs = require('fs-extra');
 const readfile = '../docs/querying/sql.md';
 const writefile = 'lib/sql-docs.js';
 
+function unwrapMarkdownLinks(str) {
+  return str.replace(/\[([^\]]+)\]\([^)]+\)/g, (_, s) => s);
+}
+
 const readDoc = async () => {
   const data = await fs.readFile(readfile, 'utf-8');
   const lines = data.split('\n');
@@ -35,7 +39,7 @@ const readDoc = async () => {
       functionDocs.push({
         name: functionMatch[1],
         arguments: functionMatch[2],
-        description: functionMatch[3],
+        description: unwrapMarkdownLinks(functionMatch[3]),
       });
     }
 
@@ -43,7 +47,9 @@ const readDoc = async () => {
     if (dataTypeMatch) {
       dataTypeDocs.push({
         name: dataTypeMatch[1],
-        description: dataTypeMatch[4] || `Druid runtime type: 
${dataTypeMatch[2]}`,
+        description: unwrapMarkdownLinks(
+          dataTypeMatch[4] || `Druid runtime type: ${dataTypeMatch[2]}`,
+        ),
       });
     }
   }
diff --git a/web-console/src/utils/general.tsx 
b/web-console/src/utils/general.tsx
index 2dc43ae..1ff1eaf 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -309,10 +309,6 @@ export function downloadFile(text: string, type: string, 
filename: string): void
   FileSaver.saveAs(blob, filename);
 }
 
-export function escapeSqlIdentifier(identifier: string): string {
-  return `"${identifier.replace(/"/g, '""')}"`;
-}
-
 export function copyAndAlert(copyString: string, alertMessage: string): void {
   copy(copyString, { format: 'text/plain' });
   AppToaster.show({
diff --git a/web-console/src/utils/index.tsx b/web-console/src/utils/index.tsx
index 89b4347..7e1cca2 100644
--- a/web-console/src/utils/index.tsx
+++ b/web-console/src/utils/index.tsx
@@ -20,4 +20,5 @@ export * from './general';
 export * from './druid-query';
 export * from './query-manager';
 export * from './query-state';
+export * from './query-cursor';
 export * from './local-storage-keys';
diff --git a/web-console/src/utils/query-cursor.ts 
b/web-console/src/utils/query-cursor.ts
new file mode 100644
index 0000000..4eb32fe
--- /dev/null
+++ b/web-console/src/utils/query-cursor.ts
@@ -0,0 +1,64 @@
+/*
+ * 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 { SqlBase, SqlLiteral, SqlQuery } from 'druid-query-toolkit';
+
+export const EMPTY_LITERAL = SqlLiteral.create('');
+
+const CRAZY_STRING = '[email protected].$';
+const DOT_DOT_DOT_LITERAL = SqlLiteral.create('...');
+
+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 interface RowColumn {
+  row: number;
+  column: number;
+}
+
+export function findEmptyLiteralPosition(query: SqlQuery): RowColumn | 
undefined {
+  const subQueryString = query
+    .walk(b => {
+      if (b === EMPTY_LITERAL) {
+        return SqlLiteral.create(CRAZY_STRING);
+      }
+      return b;
+    })
+    .toString();
+
+  const crazyIndex = subQueryString.indexOf(CRAZY_STRING);
+  if (crazyIndex < 0) return;
+
+  const prefix = subQueryString.substr(0, crazyIndex);
+  const lines = prefix.split(/\n/g);
+  const row = lines.length - 1;
+  const lastLine = lines[row];
+  return {
+    row,
+    column: lastLine.length,
+  };
+}
diff --git a/web-console/src/views/datasource-view/datasource-view.tsx 
b/web-console/src/views/datasource-view/datasource-view.tsx
index c715fd5..2b9cf31 100644
--- a/web-console/src/views/datasource-view/datasource-view.tsx
+++ b/web-console/src/views/datasource-view/datasource-view.tsx
@@ -20,6 +20,7 @@ import { FormGroup, InputGroup, Intent, MenuItem, Switch } 
from '@blueprintjs/co
 import { IconNames } from '@blueprintjs/icons';
 import axios from 'axios';
 import classNames from 'classnames';
+import { SqlQuery, SqlRef } from 'druid-query-toolkit';
 import React from 'react';
 import ReactTable, { Filter } from 'react-table';
 
@@ -41,7 +42,6 @@ import { AppToaster } from '../../singletons/toaster';
 import {
   addFilter,
   countBy,
-  escapeSqlIdentifier,
   formatBytes,
   formatNumber,
   getDruidErrorMessage,
@@ -598,7 +598,7 @@ GROUP BY 1`;
       {
         icon: IconNames.APPLICATION,
         title: 'Query with SQL',
-        onAction: () => goToQuery(`SELECT * FROM 
${escapeSqlIdentifier(datasource)}`),
+        onAction: () => 
goToQuery(SqlQuery.create(SqlRef.table(datasource)).toString()),
       },
       {
         icon: IconNames.GANTT_CHART,
diff --git 
a/web-console/src/views/query-view/__snapshots__/query-view.spec.tsx.snap 
b/web-console/src/views/query-view/__snapshots__/query-view.spec.tsx.snap
index 4348898..1f9b7f5 100644
--- a/web-console/src/views/query-view/__snapshots__/query-view.spec.tsx.snap
+++ b/web-console/src/views/query-view/__snapshots__/query-view.spec.tsx.snap
@@ -8,7 +8,7 @@ exports[`sql view matches snapshot 1`] = `
     columnMetadataLoading={true}
     defaultSchema="druid"
     getParsedQuery={[Function]}
-    onQueryStringChange={[Function]}
+    onQueryChange={[Function]}
   />
   <t
     customClassName=""
@@ -90,7 +90,7 @@ exports[`sql view matches snapshot with query 1`] = `
     columnMetadataLoading={true}
     defaultSchema="druid"
     getParsedQuery={[Function]}
-    onQueryStringChange={[Function]}
+    onQueryChange={[Function]}
   />
   <t
     customClassName=""
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 53b2bd5..ed77abe 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
@@ -17,7 +17,7 @@
  */
 
 import { render } from '@testing-library/react';
-import { parseSqlQuery } from 'druid-query-toolkit';
+import { SqlQuery } from 'druid-query-toolkit';
 import React from 'react';
 
 import { NumberMenuItems } from './number-menu-items';
@@ -29,7 +29,7 @@ describe('number menu', () => {
         schema="schema"
         table="table"
         columnName={'added'}
-        parsedQuery={parseSqlQuery(`SELECT channel, count(*) as cnt FROM 
wikipedia GROUP BY 1`)}
+        parsedQuery={SqlQuery.parse(`SELECT channel, count(*) as cnt FROM 
wikipedia GROUP BY 1`)}
         onQueryChange={() => {}}
       />
     );
@@ -44,7 +44,7 @@ describe('number menu', () => {
         schema="schema"
         table="table"
         columnName={'added'}
-        parsedQuery={parseSqlQuery(`SELECT added, count(*) as cnt FROM 
wikipedia GROUP BY 1`)}
+        parsedQuery={SqlQuery.parse(`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 ca8dc28..0c5d2b8 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,240 +18,134 @@
 
 import { MenuItem } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
-import {
-  SqlAliasRef,
-  SqlFunction,
-  SqlLiteral,
-  SqlMulti,
-  SqlQuery,
-  SqlRef,
-} from 'druid-query-toolkit';
+import { SqlExpression, SqlFunction, SqlLiteral, SqlQuery, SqlRef } from 
'druid-query-toolkit';
 import React from 'react';
 
-import { getCurrentColumns } from '../../column-tree';
+import { prettyPrintSql } from '../../../../../utils';
+
+const NINE_THOUSAND = SqlLiteral.create(9000);
 
 export interface NumberMenuItemsProps {
   table: string;
   schema: string;
   columnName: string;
   parsedQuery: SqlQuery;
-  onQueryChange: (queryString: SqlQuery, run?: boolean) => void;
+  onQueryChange: (query: SqlQuery, run?: boolean) => void;
 }
 
 export const NumberMenuItems = React.memo(function NumberMenuItems(props: 
NumberMenuItemsProps) {
   function renderFilterMenu(): JSX.Element {
     const { columnName, parsedQuery, onQueryChange } = props;
+    const ref = SqlRef.column(columnName);
 
-    return (
-      <MenuItem icon={IconNames.FILTER} text={`Filter`}>
-        <MenuItem
-          text={`"${columnName}" > 100`}
-          onClick={() => {
-            onQueryChange(
-              parsedQuery.addWhereFilter(
-                SqlRef.fromStringWithDoubleQuotes(columnName),
-                '>',
-                SqlLiteral.fromInput(100),
-              ),
-            );
-          }}
-        />
+    function filterMenuItem(clause: SqlExpression) {
+      return (
         <MenuItem
-          text={`"${columnName}" <= 100`}
+          text={prettyPrintSql(clause)}
           onClick={() => {
-            onQueryChange(
-              parsedQuery.addWhereFilter(
-                SqlRef.fromStringWithDoubleQuotes(columnName),
-                '<=',
-                SqlLiteral.fromInput(100),
-              ),
-            );
+            onQueryChange(parsedQuery.addToWhere(clause));
           }}
         />
+      );
+    }
+
+    return (
+      <MenuItem icon={IconNames.FILTER} text={`Filter`}>
+        {filterMenuItem(ref.greaterThan(NINE_THOUSAND))}
+        {filterMenuItem(ref.lessThanOrEqual(NINE_THOUSAND))}
       </MenuItem>
     );
   }
 
   function renderRemoveFilter(): JSX.Element | undefined {
     const { columnName, parsedQuery, onQueryChange } = props;
-    if (!parsedQuery.getCurrentFilters().includes(columnName)) return;
+    if (!parsedQuery.getEffectiveWhereExpression().containsColumn(columnName)) 
return;
 
     return (
       <MenuItem
         icon={IconNames.FILTER_REMOVE}
         text={`Remove filter`}
         onClick={() => {
-          onQueryChange(parsedQuery.removeFilter(columnName), true);
+          onQueryChange(parsedQuery.removeColumnFromWhere(columnName), true);
         }}
       />
     );
   }
 
+  function renderGroupByMenu(): JSX.Element | undefined {
+    const { columnName, parsedQuery, onQueryChange } = props;
+    if (!parsedQuery.hasGroupBy()) return;
+    const ref = SqlRef.column(columnName);
+
+    function groupByMenuItem(ex: SqlExpression, alias?: string) {
+      return (
+        <MenuItem
+          text={prettyPrintSql(ex)}
+          onClick={() => {
+            onQueryChange(parsedQuery.addToGroupBy(ex.as(alias)), true);
+          }}
+        />
+      );
+    }
+
+    return (
+      <MenuItem icon={IconNames.GROUP_OBJECTS} text={`Group by`}>
+        {groupByMenuItem(ref)}
+        {groupByMenuItem(
+          SqlFunction.simple('TRUNC', [ref, SqlLiteral.create(-1)]),
+          `${columnName}_truncated`,
+        )}
+      </MenuItem>
+    );
+  }
+
   function renderRemoveGroupBy(): JSX.Element | undefined {
     const { columnName, parsedQuery, onQueryChange } = props;
-    if (!parsedQuery.hasGroupByColumn(columnName)) return;
+    const selectIndex = parsedQuery.getSelectIndexForColumn(columnName);
+    if (!parsedQuery.isGroupedSelectIndex(selectIndex)) return;
+
     return (
       <MenuItem
         icon={IconNames.UNGROUP_OBJECTS}
         text={'Remove group by'}
         onClick={() => {
-          onQueryChange(parsedQuery.removeFromGroupBy(columnName), true);
+          onQueryChange(parsedQuery.removeSelectIndex(selectIndex), true);
         }}
       />
     );
   }
 
-  function renderGroupByMenu(): JSX.Element | undefined {
+  function renderAggregateMenu(): JSX.Element | undefined {
     const { columnName, parsedQuery, onQueryChange } = props;
-    if (!parsedQuery.groupByExpression) return;
+    if (!parsedQuery.hasGroupBy()) return;
+    const ref = SqlRef.column(columnName);
 
-    return (
-      <MenuItem icon={IconNames.GROUP_OBJECTS} text={`Group by`}>
+    function aggregateMenuItem(ex: SqlExpression, alias: string) {
+      return (
         <MenuItem
-          text={`"${columnName}"`}
+          text={prettyPrintSql(ex)}
           onClick={() => {
-            onQueryChange(
-              
parsedQuery.addToGroupBy(SqlRef.fromStringWithDoubleQuotes(columnName)),
-              true,
-            );
+            onQueryChange(parsedQuery.addSelectExpression(ex.as(alias)), true);
           }}
         />
-        <MenuItem
-          text={`TRUNC("${columnName}", -1) AS "${columnName}_trunc"`}
-          onClick={() => {
-            onQueryChange(
-              parsedQuery.addToGroupBy(
-                SqlAliasRef.sqlAliasFactory(
-                  SqlFunction.sqlFunctionFactory('TRUNC', [
-                    SqlRef.fromStringWithDoubleQuotes(columnName),
-                    SqlLiteral.fromInput(-1),
-                  ]),
-                  `${columnName}_truncated`,
-                ),
-              ),
-              true,
-            );
-          }}
-        />
-      </MenuItem>
-    );
-  }
-
-  function renderAggregateMenu(): JSX.Element | undefined {
-    const { columnName, parsedQuery, onQueryChange } = props;
-    if (!parsedQuery.groupByExpression) return;
+      );
+    }
 
     return (
       <MenuItem icon={IconNames.FUNCTION} text={`Aggregate`}>
-        <MenuItem
-          text={`SUM(${columnName}) AS "sum_${columnName}"`}
-          onClick={() => {
-            onQueryChange(
-              parsedQuery.addAggregateColumn(
-                [SqlRef.fromString(columnName)],
-                'SUM',
-                `sum_${columnName}`,
-              ),
-              true,
-            );
-          }}
-        />
-        <MenuItem
-          text={`MAX(${columnName}) AS "max_${columnName}"`}
-          onClick={() => {
-            onQueryChange(
-              parsedQuery.addAggregateColumn(
-                [SqlRef.fromString(columnName)],
-                'MAX',
-                `max_${columnName}`,
-              ),
-              true,
-            );
-          }}
-        />
-        <MenuItem
-          text={`MIN(${columnName}) AS "min_${columnName}"`}
-          onClick={() => {
-            onQueryChange(
-              parsedQuery.addAggregateColumn(
-                [SqlRef.fromString(columnName)],
-                'MIN',
-                `min_${columnName}`,
-              ),
-              true,
-            );
-          }}
-        />
+        {aggregateMenuItem(SqlFunction.simple('SUM', [ref]), 
`sum_${columnName}`)}
+        {aggregateMenuItem(SqlFunction.simple('MIN', [ref]), 
`min_${columnName}`)}
+        {aggregateMenuItem(SqlFunction.simple('MAX', [ref]), 
`max_${columnName}`)}
+        {aggregateMenuItem(SqlFunction.simple('AVG', [ref]), 
`avg_${columnName}`)}
+        {aggregateMenuItem(
+          SqlFunction.simple('APPROX_QUANTILE', [ref, 
SqlLiteral.create(0.98)]),
+          `p98_${columnName}`,
+        )}
+        {aggregateMenuItem(SqlFunction.simple('LATEST', [ref]), 
`latest_${columnName}`)}
       </MenuItem>
     );
   }
 
-  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()}
@@ -259,7 +153,6 @@ 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 bbaae61..3384a94 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
@@ -17,7 +17,7 @@
  */
 
 import { render } from '@testing-library/react';
-import { parseSqlQuery } from 'druid-query-toolkit';
+import { SqlQuery } from 'druid-query-toolkit';
 import React from 'react';
 
 import { StringMenuItems } from './string-menu-items';
@@ -29,7 +29,7 @@ describe('string menu', () => {
         table={'table'}
         schema={'schema'}
         columnName={'cityName'}
-        parsedQuery={parseSqlQuery(`SELECT channel, count(*) as cnt FROM 
wikipedia GROUP BY 1`)}
+        parsedQuery={SqlQuery.parse(`SELECT channel, count(*) as cnt FROM 
wikipedia GROUP BY 1`)}
         onQueryChange={() => {}}
       />
     );
@@ -44,7 +44,7 @@ describe('string menu', () => {
         table={'table'}
         schema={'schema'}
         columnName={'channel'}
-        parsedQuery={parseSqlQuery(`SELECT channel, count(*) as cnt FROM 
wikipedia GROUP BY 1`)}
+        parsedQuery={SqlQuery.parse(`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 dcba6f0..4e69cb4 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
@@ -19,57 +19,62 @@
 import { MenuItem } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
 import {
-  SqlAliasRef,
+  SqlExpression,
   SqlFunction,
+  SqlJoinPart,
   SqlLiteral,
-  SqlMulti,
   SqlQuery,
   SqlRef,
 } from 'druid-query-toolkit';
 import React from 'react';
 
-import { getCurrentColumns } from '../../column-tree';
+import { EMPTY_LITERAL, prettyPrintSql } from '../../../../../utils';
+import { getJoinColumns } from '../../column-tree';
 
 export interface StringMenuItemsProps {
   schema: string;
   table: string;
   columnName: string;
   parsedQuery: SqlQuery;
-  onQueryChange: (queryString: SqlQuery, run?: boolean) => void;
+  onQueryChange: (query: SqlQuery, run?: boolean) => void;
 }
 
 export const StringMenuItems = React.memo(function StringMenuItems(props: 
StringMenuItemsProps) {
   function renderFilterMenu(): JSX.Element | undefined {
     const { columnName, parsedQuery, onQueryChange } = props;
+    const ref = SqlRef.column(columnName);
 
-    return (
-      <MenuItem icon={IconNames.FILTER} text={`Filter`}>
-        <MenuItem
-          text={`"${columnName}" = 'xxx'`}
-          onClick={() => {
-            onQueryChange(parsedQuery.addWhereFilter(columnName, '=', 'xxx'), 
false);
-          }}
-        />
+    function filterMenuItem(clause: SqlExpression, run = true) {
+      return (
         <MenuItem
-          text={`"${columnName}" LIKE '%xxx%'`}
+          text={prettyPrintSql(clause)}
           onClick={() => {
-            onQueryChange(parsedQuery.addWhereFilter(columnName, 'LIKE', 
'%xxx%'), false);
+            onQueryChange(parsedQuery.addToWhere(clause), run);
           }}
         />
+      );
+    }
+
+    return (
+      <MenuItem icon={IconNames.FILTER} text={`Filter`}>
+        {filterMenuItem(ref.isNotNull())}
+        {filterMenuItem(ref.equal(EMPTY_LITERAL), false)}
+        {filterMenuItem(ref.like(EMPTY_LITERAL), false)}
+        {filterMenuItem(SqlFunction.simple('REGEXP_LIKE', [ref, 
EMPTY_LITERAL]), false)}
       </MenuItem>
     );
   }
 
   function renderRemoveFilter(): JSX.Element | undefined {
     const { columnName, parsedQuery, onQueryChange } = props;
-    if (!parsedQuery.getCurrentFilters().includes(columnName)) return;
+    if (!parsedQuery.getEffectiveWhereExpression().containsColumn(columnName)) 
return;
 
     return (
       <MenuItem
         icon={IconNames.FILTER_REMOVE}
         text={`Remove filter`}
         onClick={() => {
-          onQueryChange(parsedQuery.removeFilter(columnName), true);
+          onQueryChange(parsedQuery.removeColumnFromWhere(columnName), true);
         }}
       />
     );
@@ -77,13 +82,15 @@ export const StringMenuItems = React.memo(function 
StringMenuItems(props: String
 
   function renderRemoveGroupBy(): JSX.Element | undefined {
     const { columnName, parsedQuery, onQueryChange } = props;
-    if (!parsedQuery.hasGroupByColumn(columnName)) return;
+    const selectIndex = parsedQuery.getSelectIndexForColumn(columnName);
+    if (!parsedQuery.isGroupedSelectIndex(selectIndex)) return;
+
     return (
       <MenuItem
         icon={IconNames.UNGROUP_OBJECTS}
         text={'Remove group by'}
         onClick={() => {
-          onQueryChange(parsedQuery.removeFromGroupBy(columnName), true);
+          onQueryChange(parsedQuery.removeSelectIndex(selectIndex), true);
         }}
       />
     );
@@ -91,78 +98,69 @@ export const StringMenuItems = React.memo(function 
StringMenuItems(props: String
 
   function renderGroupByMenu(): JSX.Element | undefined {
     const { columnName, parsedQuery, onQueryChange } = props;
-    if (!parsedQuery.groupByExpression) return;
+    if (!parsedQuery.hasGroupBy()) return;
 
-    return (
-      <MenuItem icon={IconNames.GROUP_OBJECTS} text={`Group by`}>
-        <MenuItem
-          text={`"${columnName}"`}
-          onClick={() => {
-            onQueryChange(
-              
parsedQuery.addToGroupBy(SqlRef.fromStringWithDoubleQuotes(columnName)),
-              true,
-            );
-          }}
-        />
+    function groupByMenuItem(ex: SqlExpression, alias?: string) {
+      return (
         <MenuItem
-          text={`SUBSTRING("${columnName}", 1, 2) AS 
"${columnName}_substring"`}
+          text={prettyPrintSql(ex)}
           onClick={() => {
-            onQueryChange(
-              parsedQuery.addToGroupBy(
-                SqlAliasRef.sqlAliasFactory(
-                  SqlFunction.sqlFunctionFactory('SUBSTRING', [
-                    SqlRef.fromStringWithDoubleQuotes(columnName),
-                    SqlLiteral.fromInput(1),
-                    SqlLiteral.fromInput(2),
-                  ]),
-                  `${columnName}_substring`,
-                ),
-              ),
-              true,
-            );
+            onQueryChange(parsedQuery.addToGroupBy(alias ? ex.as(alias) : ex), 
true);
           }}
         />
+      );
+    }
+
+    return (
+      <MenuItem icon={IconNames.GROUP_OBJECTS} text={`Group by`}>
+        {groupByMenuItem(SqlRef.column(columnName))}
+        {groupByMenuItem(
+          SqlFunction.simple('SUBSTRING', [
+            SqlRef.column(columnName),
+            SqlLiteral.create(1),
+            SqlLiteral.create(2),
+          ]),
+          `${columnName}_substring`,
+        )}
+        {groupByMenuItem(
+          SqlFunction.simple('REGEXP_EXTRACT', [
+            SqlRef.column(columnName),
+            SqlLiteral.create('(\\d+)'),
+          ]),
+          `${columnName}_extract`,
+        )}
       </MenuItem>
     );
   }
 
   function renderAggregateMenu(): JSX.Element | undefined {
     const { columnName, parsedQuery, onQueryChange } = props;
-    if (!parsedQuery.groupByExpression) return;
+    if (!parsedQuery.hasGroupBy()) return;
+    const ref = SqlRef.column(columnName);
 
-    return (
-      <MenuItem icon={IconNames.FUNCTION} text={`Aggregate`}>
+    function aggregateMenuItem(ex: SqlExpression, alias: string, run = true) {
+      return (
         <MenuItem
-          text={`COUNT(DISTINCT "${columnName}") AS "dist_${columnName}"`}
-          onClick={() =>
-            onQueryChange(
-              parsedQuery.addAggregateColumn(
-                [SqlRef.fromStringWithDoubleQuotes(columnName)],
-                'COUNT',
-                `dist_${columnName}`,
-                undefined,
-                'DISTINCT',
-              ),
-              true,
-            )
-          }
-        />
-        <MenuItem
-          text={`COUNT(*) FILTER (WHERE "${columnName}" = 'xxx') AS 
${columnName}_filtered_count `}
+          text={prettyPrintSql(ex)}
           onClick={() => {
-            onQueryChange(
-              parsedQuery.addAggregateColumn(
-                [SqlRef.fromString('*')],
-                'COUNT',
-                `${columnName}_filtered_count`,
-                SqlMulti.sqlMultiFactory('=', [
-                  SqlRef.fromStringWithDoubleQuotes(columnName),
-                  SqlLiteral.fromInput('xxx'),
-                ]),
-              ),
-            );
+            onQueryChange(parsedQuery.addSelectExpression(ex.as(alias)), run);
           }}
         />
+      );
+    }
+
+    return (
+      <MenuItem icon={IconNames.FUNCTION} text={`Aggregate`}>
+        {aggregateMenuItem(SqlFunction.decorated('COUNT', 'DISTINCT', [ref]), 
`dist_${columnName}`)}
+        {aggregateMenuItem(
+          SqlFunction.simple('COUNT', [SqlRef.STAR], ref.equal(EMPTY_LITERAL)),
+          `filtered_dist_${columnName}`,
+          false,
+        )}
+        {aggregateMenuItem(
+          SqlFunction.simple('LATEST', [ref, SqlLiteral.create(100)]),
+          `latest_${columnName}`,
+        )}
       </MenuItem>
     );
   }
@@ -171,65 +169,67 @@ export const StringMenuItems = React.memo(function 
StringMenuItems(props: String
     const { schema, table, columnName, parsedQuery, onQueryChange } = props;
     if (schema !== 'lookup' || !parsedQuery) return;
 
-    const { originalTableColumn, lookupColumn } = 
getCurrentColumns(parsedQuery, table);
+    const { originalTableColumn, lookupColumn } = getJoinColumns(parsedQuery, 
table);
 
     return (
-      <>
+      <MenuItem icon={IconNames.JOIN_TABLE} text={parsedQuery.hasJoin() ? 
`Replace join` : `Join`}>
         <MenuItem
-          icon={IconNames.JOIN_TABLE}
-          text={parsedQuery.joinTable ? `Replace join` : `Join`}
-        >
-          <MenuItem
-            icon={IconNames.LEFT_JOIN}
-            text={`Left join`}
-            onClick={() => {
-              onQueryChange(
-                parsedQuery.addJoin(
+          icon={IconNames.LEFT_JOIN}
+          text={`Left join`}
+          onClick={() => {
+            onQueryChange(
+              parsedQuery.addJoin(
+                SqlJoinPart.create(
                   'LEFT',
-                  SqlRef.fromString(table, schema).upgrade(),
-                  SqlMulti.sqlMultiFactory('=', [
-                    SqlRef.fromString(columnName, table, 'lookup'),
-                    SqlRef.fromString(
+                  SqlRef.column(table, schema).upgrade(),
+                  SqlRef.column(columnName, table, 'lookup').equal(
+                    SqlRef.column(
                       lookupColumn === columnName ? originalTableColumn : 
'XXX',
-                      parsedQuery.getTableName(),
+                      parsedQuery.getFirstTableName(),
                     ),
-                  ]),
+                  ),
                 ),
-                false,
-              );
-            }}
-          />
-          <MenuItem
-            icon={IconNames.INNER_JOIN}
-            text={`Inner join`}
-            onClick={() => {
-              onQueryChange(
-                parsedQuery.addJoin(
+              ),
+              false,
+            );
+          }}
+        />
+        <MenuItem
+          icon={IconNames.INNER_JOIN}
+          text={`Inner join`}
+          onClick={() => {
+            onQueryChange(
+              parsedQuery.addJoin(
+                SqlJoinPart.create(
                   'INNER',
-                  SqlRef.fromString(table, schema).upgrade(),
-                  SqlMulti.sqlMultiFactory('=', [
-                    SqlRef.fromString(columnName, table, 'lookup'),
-                    SqlRef.fromString(
+                  SqlRef.column(table, schema).upgrade(),
+                  SqlRef.column(columnName, table, 'lookup').equal(
+                    SqlRef.column(
                       lookupColumn === columnName ? originalTableColumn : 
'XXX',
-                      parsedQuery.getTableName(),
+                      parsedQuery.getFirstTableName(),
                     ),
-                  ]),
+                  ),
                 ),
-                false,
-              );
-            }}
-          />
-        </MenuItem>
-        {parsedQuery.onExpression &&
-          parsedQuery.onExpression instanceof SqlMulti &&
-          parsedQuery.onExpression.containsColumn(columnName) && (
-            <MenuItem
-              icon={IconNames.EXCHANGE}
-              text={`Remove join`}
-              onClick={() => onQueryChange(parsedQuery.removeJoin())}
-            />
-          )}
-      </>
+              ),
+              false,
+            );
+          }}
+        />
+      </MenuItem>
+    );
+  }
+
+  function renderRemoveJoin(): JSX.Element | undefined {
+    const { schema, parsedQuery, onQueryChange } = props;
+    if (schema !== 'lookup' || !parsedQuery) return;
+    if (!parsedQuery.hasJoin()) return;
+
+    return (
+      <MenuItem
+        icon={IconNames.EXCHANGE}
+        text={`Remove join`}
+        onClick={() => onQueryChange(parsedQuery.removeAllJoins())}
+      />
     );
   }
 
@@ -241,6 +241,7 @@ export const StringMenuItems = React.memo(function 
StringMenuItems(props: String
       {renderRemoveGroupBy()}
       {renderAggregateMenu()}
       {renderJoinMenu()}
+      {renderRemoveJoin()}
     </>
   );
 });
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 5858647..f05e0ce 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
@@ -17,7 +17,7 @@
  */
 
 import { render } from '@testing-library/react';
-import { parseSqlQuery } from 'druid-query-toolkit';
+import { SqlQuery } from 'druid-query-toolkit';
 import React from 'react';
 
 import { TimeMenuItems } from './time-menu-items';
@@ -29,7 +29,7 @@ describe('time menu', () => {
         table={'table'}
         schema={'schema'}
         columnName={'__time'}
-        parsedQuery={parseSqlQuery(`SELECT channel, count(*) as cnt FROM 
wikipedia GROUP BY 1`)}
+        parsedQuery={SqlQuery.parse(`SELECT channel, count(*) as cnt FROM 
wikipedia GROUP BY 1`)}
         onQueryChange={() => {}}
       />
     );
@@ -44,7 +44,7 @@ describe('time menu', () => {
         table={'table'}
         schema={'schema'}
         columnName={'__time'}
-        parsedQuery={parseSqlQuery(`SELECT __time, count(*) as cnt FROM 
wikipedia GROUP BY 1`)}
+        parsedQuery={SqlQuery.parse(`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 e2e7e18..a319577 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
@@ -18,30 +18,45 @@
 
 import { MenuDivider, MenuItem } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
-import {
-  SqlAliasRef,
-  SqlFunction,
-  SqlInterval,
-  SqlLiteral,
-  SqlMulti,
-  SqlQuery,
-  SqlRef,
-  SqlTimestamp,
-} from 'druid-query-toolkit';
+import { SqlExpression, SqlFunction, SqlLiteral, SqlQuery, SqlRef } from 
'druid-query-toolkit';
 import React from 'react';
 
-import { getCurrentColumns } from '../../column-tree';
+import { prettyPrintSql } from '../../../../../utils';
 
-function dateToTimestamp(date: Date): SqlTimestamp {
-  return SqlTimestamp.sqlTimestampFactory(
-    date
-      .toISOString()
-      .split('.')[0]
-      .split('T')
-      .join(' '),
-  );
+const LATEST_HOUR: SqlExpression = SqlExpression.parse(
+  `? >= CURRENT_TIMESTAMP - INTERVAL '1' HOUR`,
+);
+const LATEST_DAY: SqlExpression = SqlExpression.parse(`? >= CURRENT_TIMESTAMP 
- INTERVAL '1' DAY`);
+const LATEST_WEEK: SqlExpression = SqlExpression.parse(
+  `? >= CURRENT_TIMESTAMP - INTERVAL '1' WEEK`,
+);
+const LATEST_MONTH: SqlExpression = SqlExpression.parse(
+  `? >= CURRENT_TIMESTAMP - INTERVAL '1' MONTH`,
+);
+const LATEST_YEAR: SqlExpression = SqlExpression.parse(
+  `? >= CURRENT_TIMESTAMP - INTERVAL '1' YEAR`,
+);
+
+const BETWEEN: SqlExpression = SqlExpression.parse(`(? <= ? AND ? < ?)`);
+
+// ------------------------------------
+
+function fillWithColumn(b: SqlExpression, columnName: string): SqlExpression {
+  return b.fillPlaceholders([SqlRef.column(columnName)]) as SqlExpression;
+}
+
+function fillWithColumnStartEnd(columnName: string, start: Date, end: Date): 
SqlExpression {
+  const ref = SqlRef.column(columnName);
+  return BETWEEN.fillPlaceholders([
+    SqlLiteral.create(start),
+    ref,
+    ref,
+    SqlLiteral.create(end),
+  ]) as SqlExpression;
 }
 
+// ------------------------------------
+
 function floorHour(dt: Date): Date {
   dt = new Date(dt.valueOf());
   dt.setUTCMinutes(0, 0, 0);
@@ -97,205 +112,67 @@ export interface TimeMenuItemsProps {
   schema: string;
   columnName: string;
   parsedQuery: SqlQuery;
-  onQueryChange: (queryString: SqlQuery, run?: boolean) => void;
+  onQueryChange: (query: SqlQuery, run?: boolean) => void;
 }
 
 export const TimeMenuItems = React.memo(function TimeMenuItems(props: 
TimeMenuItemsProps) {
   function renderFilterMenu(): JSX.Element | undefined {
     const { columnName, parsedQuery, onQueryChange } = props;
-    const now = new Date();
 
-    return (
-      <MenuItem icon={IconNames.FILTER} text={`Filter`}>
-        <MenuItem
-          text={`Latest hour`}
-          onClick={() => {
-            onQueryChange(
-              parsedQuery
-                .removeFilter(columnName)
-                .addWhereFilter(
-                  columnName,
-                  '>=',
-                  SqlMulti.sqlMultiFactory('-', [
-                    SqlRef.fromString('CURRENT_TIMESTAMP'),
-                    SqlInterval.sqlIntervalFactory('HOUR', 1),
-                  ]),
-                ),
-              true,
-            );
-          }}
-        />
-        <MenuItem
-          text={`Latest day`}
-          onClick={() => {
-            onQueryChange(
-              parsedQuery
-                .removeFilter(columnName)
-                .addWhereFilter(
-                  columnName,
-                  '>=',
-                  SqlMulti.sqlMultiFactory('-', [
-                    SqlRef.fromString('CURRENT_TIMESTAMP'),
-                    SqlInterval.sqlIntervalFactory('Day', 1),
-                  ]),
-                ),
-              true,
-            );
-          }}
-        />
-        <MenuItem
-          text={`Latest week`}
-          onClick={() => {
-            onQueryChange(
-              parsedQuery
-                .removeFilter(columnName)
-                .addWhereFilter(
-                  columnName,
-                  '>=',
-                  SqlMulti.sqlMultiFactory('-', [
-                    SqlRef.fromString('CURRENT_TIMESTAMP'),
-                    SqlInterval.sqlIntervalFactory('Day', 7),
-                  ]),
-                ),
-              true,
-            );
-          }}
-        />
-        <MenuItem
-          text={`Latest month`}
-          onClick={() => {
-            onQueryChange(
-              parsedQuery
-                .removeFilter(columnName)
-                .addWhereFilter(
-                  columnName,
-                  '>=',
-                  SqlMulti.sqlMultiFactory('-', [
-                    SqlRef.fromString('CURRENT_TIMESTAMP'),
-                    SqlInterval.sqlIntervalFactory('MONTH', 1),
-                  ]),
-                ),
-              true,
-            );
-          }}
-        />
+    function filterMenuItem(label: string, clause: SqlExpression) {
+      return (
         <MenuItem
-          text={`Latest year`}
+          text={label}
           onClick={() => {
-            onQueryChange(
-              parsedQuery
-                .removeFilter(columnName)
-                .addWhereFilter(
-                  columnName,
-                  '>=',
-                  SqlMulti.sqlMultiFactory('-', [
-                    SqlRef.fromString('CURRENT_TIMESTAMP'),
-                    SqlInterval.sqlIntervalFactory('YEAR', 1),
-                  ]),
-                ),
-              true,
-            );
+            
onQueryChange(parsedQuery.removeColumnFromWhere(columnName).addToWhere(clause), 
true);
           }}
         />
+      );
+    }
+
+    const now = new Date();
+    const hourStart = floorHour(now);
+    const dayStart = floorDay(now);
+    const monthStart = floorMonth(now);
+    const yearStart = floorYear(now);
+    return (
+      <MenuItem icon={IconNames.FILTER} text={`Filter`}>
+        {filterMenuItem(`Latest hour`, fillWithColumn(LATEST_HOUR, 
columnName))}
+        {filterMenuItem(`Latest day`, fillWithColumn(LATEST_DAY, columnName))}
+        {filterMenuItem(`Latest week`, fillWithColumn(LATEST_WEEK, 
columnName))}
+        {filterMenuItem(`Latest month`, fillWithColumn(LATEST_MONTH, 
columnName))}
+        {filterMenuItem(`Latest year`, fillWithColumn(LATEST_YEAR, 
columnName))}
         <MenuDivider />
-        <MenuItem
-          text={`Current hour`}
-          onClick={() => {
-            const hourStart = floorHour(now);
-            onQueryChange(
-              parsedQuery
-                .removeFilter(columnName)
-                .addWhereFilter(
-                  SqlRef.fromStringWithDoubleQuotes(columnName),
-                  '>=',
-                  dateToTimestamp(hourStart),
-                )
-                .addWhereFilter(
-                  SqlRef.fromStringWithDoubleQuotes(columnName),
-                  '<',
-                  dateToTimestamp(nextHour(hourStart)),
-                ),
-              true,
-            );
-          }}
-        />
-        <MenuItem
-          text={`Current day`}
-          onClick={() => {
-            const dayStart = floorDay(now);
-            onQueryChange(
-              parsedQuery
-                .removeFilter(columnName)
-                .addWhereFilter(
-                  SqlRef.fromStringWithDoubleQuotes(columnName),
-                  '>=',
-                  dateToTimestamp(dayStart),
-                )
-                .addWhereFilter(
-                  SqlRef.fromStringWithDoubleQuotes(columnName),
-                  '<',
-                  dateToTimestamp(nextDay(dayStart)),
-                ),
-              true,
-            );
-          }}
-        />
-        <MenuItem
-          text={`Current month`}
-          onClick={() => {
-            const monthStart = floorMonth(now);
-            onQueryChange(
-              parsedQuery
-                .removeFilter(columnName)
-                .addWhereFilter(
-                  SqlRef.fromStringWithDoubleQuotes(columnName),
-                  '>=',
-                  dateToTimestamp(monthStart),
-                )
-                .addWhereFilter(
-                  SqlRef.fromStringWithDoubleQuotes(columnName),
-                  '<',
-                  dateToTimestamp(nextMonth(monthStart)),
-                ),
-              true,
-            );
-          }}
-        />
-        <MenuItem
-          text={`Current year`}
-          onClick={() => {
-            const yearStart = floorYear(now);
-            onQueryChange(
-              parsedQuery
-                .removeFilter(columnName)
-                .addWhereFilter(
-                  SqlRef.fromStringWithDoubleQuotes(columnName),
-                  '<=',
-                  dateToTimestamp(yearStart),
-                )
-                .addWhereFilter(
-                  dateToTimestamp(yearStart),
-                  '<',
-                  dateToTimestamp(nextYear(yearStart)),
-                ),
-              true,
-            );
-          }}
-        />
+        {filterMenuItem(
+          `Current hour`,
+          fillWithColumnStartEnd(columnName, hourStart, nextHour(hourStart)),
+        )}
+        {filterMenuItem(
+          `Current day`,
+          fillWithColumnStartEnd(columnName, dayStart, nextDay(dayStart)),
+        )}
+        {filterMenuItem(
+          `Current month`,
+          fillWithColumnStartEnd(columnName, monthStart, 
nextMonth(monthStart)),
+        )}
+        {filterMenuItem(
+          `Current year`,
+          fillWithColumnStartEnd(columnName, yearStart, nextYear(yearStart)),
+        )}
       </MenuItem>
     );
   }
 
   function renderRemoveFilter(): JSX.Element | undefined {
     const { columnName, parsedQuery, onQueryChange } = props;
-    if (!parsedQuery.getCurrentFilters().includes(columnName)) return;
+    if (!parsedQuery.getEffectiveWhereExpression().containsColumn(columnName)) 
return;
 
     return (
       <MenuItem
         icon={IconNames.FILTER_REMOVE}
         text={`Remove filter`}
         onClick={() => {
-          onQueryChange(parsedQuery.removeFilter(columnName), true);
+          onQueryChange(parsedQuery.removeColumnFromWhere(columnName), true);
         }}
       />
     );
@@ -303,13 +180,15 @@ export const TimeMenuItems = React.memo(function 
TimeMenuItems(props: TimeMenuIt
 
   function renderRemoveGroupBy(): JSX.Element | undefined {
     const { columnName, parsedQuery, onQueryChange } = props;
-    if (!parsedQuery.hasGroupByColumn(columnName)) return;
+    const selectIndex = parsedQuery.getSelectIndexForColumn(columnName);
+    if (!parsedQuery.isGroupedSelectIndex(selectIndex)) return;
+
     return (
       <MenuItem
         icon={IconNames.UNGROUP_OBJECTS}
         text={'Remove group by'}
         onClick={() => {
-          onQueryChange(parsedQuery.removeFromGroupBy(columnName), true);
+          onQueryChange(parsedQuery.removeSelectIndex(selectIndex), true);
         }}
       />
     );
@@ -317,164 +196,80 @@ export const TimeMenuItems = React.memo(function 
TimeMenuItems(props: TimeMenuIt
 
   function renderGroupByMenu(): JSX.Element | undefined {
     const { columnName, parsedQuery, onQueryChange } = props;
-    if (!parsedQuery.groupByExpression) return;
+    if (!parsedQuery.hasGroupBy()) return;
+    const ref = SqlRef.column(columnName);
 
-    return (
-      <MenuItem icon={IconNames.GROUP_OBJECTS} text={`Group by`}>
-        <MenuItem
-          text={`TIME_FLOOR("${columnName}", 'PT1H') AS 
"${columnName}_time_floor"`}
-          onClick={() => {
-            onQueryChange(
-              parsedQuery.addToGroupBy(
-                SqlAliasRef.sqlAliasFactory(
-                  SqlFunction.sqlFunctionFactory('TIME_FLOOR', [
-                    SqlRef.fromString(columnName),
-                    SqlLiteral.fromInput('PT1h'),
-                  ]),
-                  `${columnName}_time_floor`,
-                ),
-              ),
-              true,
-            );
-          }}
-        />
-        <MenuItem
-          text={`TIME_FLOOR("${columnName}", 'P1D') AS 
"${columnName}_time_floor"`}
-          onClick={() => {
-            onQueryChange(
-              parsedQuery.addToGroupBy(
-                SqlAliasRef.sqlAliasFactory(
-                  SqlFunction.sqlFunctionFactory('TIME_FLOOR', [
-                    SqlRef.fromString(columnName),
-                    SqlLiteral.fromInput('P1D'),
-                  ]),
-                  `${columnName}_time_floor`,
-                ),
-              ),
-              true,
-            );
-          }}
-        />
+    function groupByMenuItem(ex: SqlExpression, alias: string) {
+      return (
         <MenuItem
-          text={`TIME_FLOOR("${columnName}", 'P7D') AS 
"${columnName}_time_floor"`}
+          text={prettyPrintSql(ex)}
           onClick={() => {
-            onQueryChange(
-              parsedQuery.addToGroupBy(
-                SqlAliasRef.sqlAliasFactory(
-                  SqlFunction.sqlFunctionFactory('TIME_FLOOR', [
-                    SqlRef.fromString(columnName),
-                    SqlLiteral.fromInput('P7D'),
-                  ]),
-                  `${columnName}_time_floor`,
-                ),
-              ),
-              true,
-            );
+            onQueryChange(parsedQuery.addToGroupBy(ex.as(alias)), true);
           }}
         />
+      );
+    }
+
+    return (
+      <MenuItem icon={IconNames.GROUP_OBJECTS} text={`Group by`}>
+        {groupByMenuItem(
+          SqlFunction.simple('TIME_FLOOR', [ref, SqlLiteral.create('PT1H')]),
+          `${columnName}_by_hour`,
+        )}
+        {groupByMenuItem(
+          SqlFunction.simple('TIME_FLOOR', [ref, SqlLiteral.create('P1D')]),
+          `${columnName}_by_day`,
+        )}
+        {groupByMenuItem(
+          SqlFunction.simple('TIME_FLOOR', [ref, SqlLiteral.create('P1M')]),
+          `${columnName}_by_month`,
+        )}
+        {groupByMenuItem(
+          SqlFunction.simple('TIME_FLOOR', [ref, SqlLiteral.create('P1Y')]),
+          `${columnName}_by_year`,
+        )}
+        <MenuDivider />
+        {groupByMenuItem(
+          SqlFunction.simple('TIME_EXTRACT', [ref, SqlLiteral.create('HOUR')]),
+          `hour_of_${columnName}`,
+        )}
+        {groupByMenuItem(
+          SqlFunction.simple('TIME_EXTRACT', [ref, SqlLiteral.create('DAY')]),
+          `day_of_${columnName}`,
+        )}
+        {groupByMenuItem(
+          SqlFunction.simple('TIME_EXTRACT', [ref, 
SqlLiteral.create('MONTH')]),
+          `month_of_${columnName}`,
+        )}
+        {groupByMenuItem(
+          SqlFunction.simple('TIME_EXTRACT', [ref, SqlLiteral.create('YEAR')]),
+          `year_of_${columnName}`,
+        )}
       </MenuItem>
     );
   }
 
   function renderAggregateMenu(): JSX.Element | undefined {
     const { columnName, parsedQuery, onQueryChange } = props;
-    if (!parsedQuery.groupByExpression) return;
+    if (!parsedQuery.hasGroupBy()) return;
+    const ref = SqlRef.column(columnName);
 
-    return (
-      <MenuItem icon={IconNames.FUNCTION} text={`Aggregate`}>
-        <MenuItem
-          text={`MAX("${columnName}") AS "max_${columnName}"`}
-          onClick={() => {
-            onQueryChange(
-              parsedQuery.addAggregateColumn(
-                [SqlRef.fromStringWithDoubleQuotes(columnName)],
-                'MAX',
-                `max_${columnName}`,
-              ),
-              true,
-            );
-          }}
-        />
+    function aggregateMenuItem(ex: SqlExpression, alias: string) {
+      return (
         <MenuItem
-          text={`MIN("${columnName}") AS "min_${columnName}"`}
+          text={prettyPrintSql(ex)}
           onClick={() => {
-            onQueryChange(
-              parsedQuery.addAggregateColumn(
-                [SqlRef.fromStringWithDoubleQuotes(columnName)],
-                'MIN',
-                `min_${columnName}`,
-              ),
-              true,
-            );
+            onQueryChange(parsedQuery.addSelectExpression(ex.as(alias)), true);
           }}
         />
-      </MenuItem>
-    );
-  }
-
-  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())}
-            />
-          )}
-      </>
+      <MenuItem icon={IconNames.FUNCTION} text={`Aggregate`}>
+        {aggregateMenuItem(SqlFunction.simple('MAX', [ref]), 
`max_${columnName}`)}
+        {aggregateMenuItem(SqlFunction.simple('MIN', [ref]), 
`min_${columnName}`)}
+      </MenuItem>
     );
   }
 
@@ -485,7 +280,6 @@ 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.spec.tsx 
b/web-console/src/views/query-view/column-tree/column-tree.spec.tsx
index 5636c74..b228a63 100644
--- a/web-console/src/views/query-view/column-tree/column-tree.spec.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree.spec.tsx
@@ -17,7 +17,7 @@
  */
 
 import { render } from '@testing-library/react';
-import { parseSqlQuery } from 'druid-query-toolkit';
+import { SqlQuery } from 'druid-query-toolkit';
 import React from 'react';
 
 import { ColumnMetadata } from '../../../utils/column-metadata';
@@ -29,7 +29,7 @@ describe('column tree', () => {
     const columnTree = (
       <ColumnTree
         getParsedQuery={() => {
-          return parseSqlQuery(`SELECT channel, count(*) as cnt FROM wikipedia 
GROUP BY 1`);
+          return SqlQuery.parse(`SELECT channel, count(*) as cnt FROM 
wikipedia GROUP BY 1`);
         }}
         defaultSchema="druid"
         defaultTable="wikipedia"
@@ -62,7 +62,7 @@ describe('column tree', () => {
             },
           ] as ColumnMetadata[]
         }
-        onQueryStringChange={() => {}}
+        onQueryChange={() => {}}
       />
     );
 
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 d672b07..5bd4d13 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,93 +27,96 @@ import {
   Tree,
 } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
-import { SqlMulti, SqlQuery, SqlRef } from 'druid-query-toolkit';
+import {
+  SqlAlias,
+  SqlComparison,
+  SqlExpression,
+  SqlFunction,
+  SqlJoinPart,
+  SqlQuery,
+  SqlRef,
+} from 'druid-query-toolkit';
 import React, { ChangeEvent } from 'react';
 
 import { Loader } from '../../../components';
 import { Deferred } from '../../../components/deferred/deferred';
-import { copyAndAlert, escapeSqlIdentifier, groupBy } from '../../../utils';
+import { copyAndAlert, groupBy, prettyPrintSql } from '../../../utils';
 import { ColumnMetadata } from '../../../utils/column-metadata';
 
 import { NumberMenuItems, StringMenuItems, TimeMenuItems } from 
'./column-tree-menu';
 
 import './column-tree.scss';
 
-function handleTableClick(
-  tableSchema: string,
-  nodeData: ITreeNode,
-  onQueryStringChange: (queryString: string, run: boolean) => void,
-): void {
-  let columns: string[];
-  if (nodeData.childNodes) {
-    columns = nodeData.childNodes.map(child => 
escapeSqlIdentifier(String(child.label)));
-  } else {
-    columns = ['*'];
-  }
-  if (tableSchema === 'druid') {
-    onQueryStringChange(
-      `SELECT ${columns.join(', ')}
-FROM ${escapeSqlIdentifier(String(nodeData.label))}
-WHERE "__time" >= CURRENT_TIMESTAMP - INTERVAL '1' DAY`,
-      true,
-    );
-  } else {
-    onQueryStringChange(
-      `SELECT ${columns.join(', ')}
-FROM ${tableSchema}.${nodeData.label}`,
-      true,
-    );
-  }
+const LAST_DAY = SqlExpression.parse(`__time >= CURRENT_TIMESTAMP - INTERVAL 
'1' DAY`);
+const COUNT_STAR = SqlFunction.COUNT_STAR.as('Count');
+
+const STRING_QUERY = SqlQuery.parse(`SELECT
+  ?
+FROM ?
+GROUP BY 1
+ORDER BY 2 DESC`);
+
+const TIME_QUERY = SqlQuery.parse(`SELECT
+  TIME_FLOOR(?, 'PT1H') AS "Time"
+FROM ?
+GROUP BY 1
+ORDER BY 1 ASC`);
+
+interface HandleColumnClickOptions {
+  columnSchema: string;
+  columnTable: string;
+  columnName: string;
+  columnType: string;
+  parsedQuery: SqlQuery | undefined;
+  onQueryChange: (query: SqlQuery, run: boolean) => void;
 }
 
-function handleColumnClick(
-  columnSchema: string,
-  columnTable: string,
-  nodeData: ITreeNode,
-  onQueryStringChange: (queryString: string, run: boolean) => void,
-): void {
+function handleColumnClick(options: HandleColumnClickOptions): void {
+  const { columnSchema, columnTable, columnName, columnType, parsedQuery, 
onQueryChange } = options;
+
+  let query: SqlQuery;
+  const columnRef = SqlRef.column(columnName);
   if (columnSchema === 'druid') {
-    if (nodeData.icon === IconNames.TIME) {
-      onQueryStringChange(
-        `SELECT
-  TIME_FLOOR(${escapeSqlIdentifier(String(nodeData.label))}, 'PT1H') AS "Time",
-  COUNT(*) AS "Count"
-FROM ${escapeSqlIdentifier(columnTable)}
-WHERE "__time" >= CURRENT_TIMESTAMP - INTERVAL '1' DAY
-GROUP BY 1
-ORDER BY "Time" ASC`,
-        true,
-      );
+    if (columnType === 'TIMESTAMP') {
+      query = TIME_QUERY.fillPlaceholders([columnRef, 
SqlRef.table(columnTable)]) as SqlQuery;
     } else {
-      onQueryStringChange(
-        `SELECT
-  "${nodeData.label}",
-  COUNT(*) AS "Count"
-FROM ${escapeSqlIdentifier(columnTable)}
-WHERE "__time" >= CURRENT_TIMESTAMP - INTERVAL '1' DAY
-GROUP BY 1
-ORDER BY "Count" DESC`,
-        true,
-      );
+      query = STRING_QUERY.fillPlaceholders([columnRef, 
SqlRef.table(columnTable)]) as SqlQuery;
     }
   } else {
-    onQueryStringChange(
-      `SELECT
-  ${escapeSqlIdentifier(String(nodeData.label))},
-  COUNT(*) AS "Count"
-FROM ${columnSchema}.${columnTable}
-GROUP BY 1
-ORDER BY "Count" DESC`,
-      true,
-    );
+    query = STRING_QUERY.fillPlaceholders([
+      columnRef,
+      SqlRef.table(columnTable, columnSchema),
+    ]) as SqlQuery;
   }
+
+  let where: SqlExpression | undefined;
+  let aggregates: SqlAlias[] = [];
+  if (parsedQuery && parsedQuery.getFirstTableName() === columnTable) {
+    where = parsedQuery.getWhereExpression();
+    aggregates = parsedQuery.getAggregateSelectExpressions();
+  } else if (columnSchema === 'druid') {
+    where = LAST_DAY;
+  }
+  if (!aggregates.length) {
+    aggregates.push(COUNT_STAR);
+  }
+
+  let newSelectExpressions = query.selectExpressions;
+  for (const aggregate of aggregates) {
+    newSelectExpressions = newSelectExpressions.addLast(aggregate);
+  }
+
+  onQueryChange(
+    
query.changeSelectExpressions(newSelectExpressions).changeWhereExpression(where),
+    true,
+  );
 }
 
 export interface ColumnTreeProps {
   columnMetadataLoading: boolean;
   columnMetadata?: readonly ColumnMetadata[];
   getParsedQuery: () => SqlQuery | undefined;
-  onQueryStringChange: (queryString: string | SqlQuery, run?: boolean) => void;
+  onQueryChange: (query: SqlQuery, run?: boolean) => void;
   defaultSchema?: string;
   defaultTable?: string;
 }
@@ -125,48 +128,44 @@ 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;
-        }
+export function getJoinColumns(parsedQuery: SqlQuery, _table: string) {
+  let lookupColumn: string | undefined;
+  let originalTableColumn: string | undefined;
+  if (parsedQuery.fromClause && parsedQuery.fromClause.joinParts) {
+    const firstOnExpression = 
parsedQuery.fromClause.joinParts.first().onExpression;
+    if (firstOnExpression instanceof SqlComparison && firstOnExpression.op === 
'=') {
+      const { lhs, rhs } = firstOnExpression;
+      if (lhs instanceof SqlRef && lhs.namespace === 'lookup') {
+        lookupColumn = lhs.column;
       }
-    });
+      if (rhs instanceof SqlRef) {
+        originalTableColumn = rhs.column;
+      }
+    }
   }
 
   return {
-    lookupColumn: lookupColumn || 'XXX',
+    lookupColumn: lookupColumn || 'k',
     originalTableColumn: originalTableColumn || 'XXX',
   };
 }
 
 export class ColumnTree extends React.PureComponent<ColumnTreeProps, 
ColumnTreeState> {
   static getDerivedStateFromProps(props: ColumnTreeProps, state: 
ColumnTreeState) {
-    const { columnMetadata, defaultSchema, defaultTable } = props;
+    const { columnMetadata, defaultSchema, defaultTable, onQueryChange } = 
props;
 
     if (columnMetadata && columnMetadata !== state.prevColumnMetadata) {
       const columnTree = groupBy(
         columnMetadata,
         r => r.TABLE_SCHEMA,
-        (metadata, schema): ITreeNode => ({
-          id: schema,
-          label: schema,
+        (metadata, schemaName): ITreeNode => ({
+          id: schemaName,
+          label: schemaName,
           childNodes: groupBy(
             metadata,
             r => r.TABLE_NAME,
-            (metadata, table): ITreeNode => ({
-              id: table,
+            (metadata, tableName): ITreeNode => ({
+              id: tableName,
               icon: IconNames.TH,
               label: (
                 <Popover
@@ -176,70 +175,77 @@ export class ColumnTree extends 
React.PureComponent<ColumnTreeProps, ColumnTreeS
                     <Deferred
                       content={() => {
                         const parsedQuery = props.getParsedQuery();
+                        const tableRef = SqlRef.table(tableName).as();
+                        const prettyTableRef = prettyPrintSql(tableRef);
                         return (
                           <Menu>
                             <MenuItem
                               icon={IconNames.FULLSCREEN}
-                              text={`SELECT ... FROM ${table}`}
+                              text={`SELECT ... FROM ${tableName}`}
                               onClick={() => {
-                                handleTableClick(
-                                  schema,
-                                  {
-                                    id: table,
-                                    icon: IconNames.TH,
-                                    label: table,
-                                    childNodes: metadata.map(columnData => ({
-                                      id: columnData.COLUMN_NAME,
-                                      icon: 
ColumnTree.dataTypeToIcon(columnData.DATA_TYPE),
-                                      label: columnData.COLUMN_NAME,
-                                    })),
-                                  },
-                                  props.onQueryStringChange,
+                                const tableRef = SqlRef.table(
+                                  tableName,
+                                  schemaName === 'druid' ? undefined : 
schemaName,
+                                );
+
+                                let where: SqlExpression | undefined;
+                                if (parsedQuery && 
parsedQuery.getFirstTableName() === tableName) {
+                                  where = parsedQuery.getWhereExpression();
+                                } else if (schemaName === 'druid') {
+                                  where = LAST_DAY;
+                                }
+
+                                onQueryChange(
+                                  SqlQuery.create(tableRef)
+                                    .changeSelectExpressions(
+                                      metadata.map(child => 
SqlRef.column(child.COLUMN_NAME).as()),
+                                    )
+                                    .changeWhereExpression(where),
+                                  true,
                                 );
                               }}
                             />
-                            <MenuItem
-                              icon={IconNames.CLIPBOARD}
-                              text={`Copy: ${table}`}
-                              onClick={() => {
-                                copyAndAlert(table, `${table} query copied to 
clipboard`);
-                              }}
-                            />
-                            {parsedQuery && (
+                            {parsedQuery && parsedQuery.getFirstTableName() 
!== tableName && (
                               <MenuItem
                                 icon={IconNames.EXCHANGE}
-                                text={`Replace FROM with: ${table}`}
+                                text={`Replace FROM with: ${prettyTableRef}`}
                                 onClick={() => {
-                                  
props.onQueryStringChange(parsedQuery.replaceFrom(table), true);
+                                  onQueryChange(
+                                    
parsedQuery.changeFromExpressions([tableRef]),
+                                    true,
+                                  );
                                 }}
                               />
                             )}
-                            {parsedQuery && schema === 'lookup' && (
+                            {parsedQuery && schemaName === 'lookup' && (
                               <MenuItem
                                 popoverProps={{ openOnTargetFocus: false }}
                                 icon={IconNames.JOIN_TABLE}
-                                text={parsedQuery.joinTable ? `Replace join` : 
`Join`}
+                                text={parsedQuery.hasJoin() ? `Replace join` : 
`Join`}
                               >
                                 <MenuItem
                                   icon={IconNames.LEFT_JOIN}
                                   text={`Left join`}
                                   onClick={() => {
-                                    const { lookupColumn, originalTableColumn 
} = getCurrentColumns(
+                                    const { lookupColumn, originalTableColumn 
} = getJoinColumns(
                                       parsedQuery,
-                                      table,
+                                      tableName,
                                     );
-                                    props.onQueryStringChange(
-                                      parsedQuery.addJoin(
-                                        'LEFT',
-                                        SqlRef.fromString(table, 
schema).upgrade(),
-                                        SqlMulti.sqlMultiFactory('=', [
-                                          SqlRef.fromString(lookupColumn, 
table, 'lookup'),
-                                          SqlRef.fromString(
-                                            originalTableColumn,
-                                            parsedQuery.getTableName(),
+                                    onQueryChange(
+                                      parsedQuery
+                                        .removeAllJoins()
+                                        .addJoin(
+                                          SqlJoinPart.create(
+                                            'LEFT',
+                                            SqlRef.column(tableName, 
schemaName).upgrade(),
+                                            SqlRef.column(lookupColumn, 
tableName, 'lookup').equal(
+                                              SqlRef.column(
+                                                originalTableColumn,
+                                                
parsedQuery.getFirstTableName(),
+                                              ),
+                                            ),
                                           ),
-                                        ]),
-                                      ),
+                                        ),
                                       false,
                                     );
                                   }}
@@ -248,21 +254,22 @@ export class ColumnTree extends 
React.PureComponent<ColumnTreeProps, ColumnTreeS
                                   icon={IconNames.INNER_JOIN}
                                   text={`Inner join`}
                                   onClick={() => {
-                                    const { lookupColumn, originalTableColumn 
} = getCurrentColumns(
+                                    const { lookupColumn, originalTableColumn 
} = getJoinColumns(
                                       parsedQuery,
-                                      table,
+                                      tableName,
                                     );
-                                    props.onQueryStringChange(
+                                    onQueryChange(
                                       parsedQuery.addJoin(
-                                        'INNER',
-                                        SqlRef.fromString(table, 
schema).upgrade(),
-                                        SqlMulti.sqlMultiFactory('=', [
-                                          SqlRef.fromString(lookupColumn, 
table, 'lookup'),
-                                          SqlRef.fromString(
-                                            originalTableColumn,
-                                            parsedQuery.getTableName(),
+                                        SqlJoinPart.create(
+                                          'INNER',
+                                          SqlRef.column(tableName, 
schemaName).upgrade(),
+                                          SqlRef.column(lookupColumn, 
tableName, 'lookup').equal(
+                                            SqlRef.column(
+                                              originalTableColumn,
+                                              parsedQuery.getFirstTableName(),
+                                            ),
                                           ),
-                                        ]),
+                                        ),
                                       ),
                                       false,
                                     );
@@ -271,23 +278,42 @@ export class ColumnTree extends 
React.PureComponent<ColumnTreeProps, ColumnTreeS
                               </MenuItem>
                             )}
                             {parsedQuery &&
-                              parsedQuery.joinTable &&
-                              parsedQuery.joinTable.table === table && (
+                              parsedQuery.hasJoin() &&
+                              parsedQuery.getJoins()[0].table.toString() === 
tableName && (
                                 <MenuItem
                                   icon={IconNames.EXCHANGE}
                                   text={`Remove join`}
+                                  onClick={() => 
onQueryChange(parsedQuery.removeAllJoins())}
+                                />
+                              )}
+                            {parsedQuery &&
+                              parsedQuery.hasGroupBy() &&
+                              parsedQuery.getFirstTableName() === tableName && 
(
+                                <MenuItem
+                                  icon={IconNames.FUNCTION}
+                                  text={`Aggregate COUNT(*)`}
                                   onClick={() =>
-                                    
props.onQueryStringChange(parsedQuery.removeJoin())
+                                    
onQueryChange(parsedQuery.addSelectExpression(COUNT_STAR), true)
                                   }
                                 />
                               )}
+                            <MenuItem
+                              icon={IconNames.CLIPBOARD}
+                              text={`Copy: ${prettyTableRef}`}
+                              onClick={() => {
+                                copyAndAlert(
+                                  tableRef.toString(),
+                                  `${prettyTableRef} query copied to 
clipboard`,
+                                );
+                              }}
+                            />
                           </Menu>
                         );
                       }}
                     />
                   }
                 >
-                  <div>{table}</div>
+                  <div>{tableName}</div>
                 </Popover>
               ),
               childNodes: metadata
@@ -311,45 +337,43 @@ export class ColumnTree extends 
React.PureComponent<ColumnTreeProps, ColumnTreeS
                                     icon={IconNames.FULLSCREEN}
                                     text={`Show: ${columnData.COLUMN_NAME}`}
                                     onClick={() => {
-                                      handleColumnClick(
-                                        schema,
-                                        table,
-                                        {
-                                          id: columnData.COLUMN_NAME,
-                                          icon: 
ColumnTree.dataTypeToIcon(columnData.DATA_TYPE),
-                                          label: columnData.COLUMN_NAME,
-                                        },
-                                        props.onQueryStringChange,
-                                      );
+                                      handleColumnClick({
+                                        columnSchema: schemaName,
+                                        columnTable: tableName,
+                                        columnName: columnData.COLUMN_NAME,
+                                        columnType: columnData.DATA_TYPE,
+                                        parsedQuery,
+                                        onQueryChange: onQueryChange,
+                                      });
                                     }}
                                   />
                                   {parsedQuery &&
                                     (columnData.DATA_TYPE === 'BIGINT' ||
                                       columnData.DATA_TYPE === 'FLOAT') && (
                                       <NumberMenuItems
-                                        table={table}
-                                        schema={schema}
+                                        table={tableName}
+                                        schema={schemaName}
                                         columnName={columnData.COLUMN_NAME}
                                         parsedQuery={parsedQuery}
-                                        
onQueryChange={props.onQueryStringChange}
+                                        onQueryChange={onQueryChange}
                                       />
                                     )}
                                   {parsedQuery && columnData.DATA_TYPE === 
'VARCHAR' && (
                                     <StringMenuItems
-                                      table={table}
-                                      schema={schema}
+                                      table={tableName}
+                                      schema={schemaName}
                                       columnName={columnData.COLUMN_NAME}
                                       parsedQuery={parsedQuery}
-                                      onQueryChange={props.onQueryStringChange}
+                                      onQueryChange={onQueryChange}
                                     />
                                   )}
                                   {parsedQuery && columnData.DATA_TYPE === 
'TIMESTAMP' && (
                                     <TimeMenuItems
-                                      table={table}
-                                      schema={schema}
+                                      table={tableName}
+                                      schema={schemaName}
                                       columnName={columnData.COLUMN_NAME}
                                       parsedQuery={parsedQuery}
-                                      onQueryChange={props.onQueryStringChange}
+                                      onQueryChange={onQueryChange}
                                     />
                                   )}
                                   <MenuItem
diff --git 
a/web-console/src/views/query-view/query-extra-info/__snapshots__/query-extra-info.spec.tsx.snap
 
b/web-console/src/views/query-view/query-extra-info/__snapshots__/query-extra-info.spec.tsx.snap
index 9984baa..310ea13 100644
--- 
a/web-console/src/views/query-view/query-extra-info/__snapshots__/query-extra-info.spec.tsx.snap
+++ 
b/web-console/src/views/query-view/query-extra-info/__snapshots__/query-extra-info.spec.tsx.snap
@@ -17,7 +17,7 @@ exports[`query extra info matches snapshot 1`] = `
           class=""
           tabindex="0"
         >
-          999+ results in 8.00s
+          0 results in 8.00s
         </span>
       </span>
     </span>
diff --git 
a/web-console/src/views/query-view/query-extra-info/query-extra-info.spec.tsx 
b/web-console/src/views/query-view/query-extra-info/query-extra-info.spec.tsx
index 621942e..9c43073 100644
--- 
a/web-console/src/views/query-view/query-extra-info/query-extra-info.spec.tsx
+++ 
b/web-console/src/views/query-view/query-extra-info/query-extra-info.spec.tsx
@@ -17,6 +17,7 @@
  */
 
 import { render } from '@testing-library/react';
+import { QueryResult } from 'druid-query-toolkit';
 import React from 'react';
 
 import { QueryExtraInfo } from './query-extra-info';
@@ -25,13 +26,9 @@ describe('query extra info', () => {
   it('matches snapshot', () => {
     const queryExtraInfo = (
       <QueryExtraInfo
-        queryExtraInfo={{
-          queryId: 'e3ee781b-c0b6-4385-9d99-a8a1994bebac',
-          startTime: new Date('1986-04-26T01:23:40+03:00'),
-          endTime: new Date('1986-04-26T01:23:48+03:00'),
-          numResults: 1000,
-          wrapQueryLimit: 1000,
-        }}
+        queryResult={QueryResult.BLANK.attachQueryId(
+          'e3ee781b-c0b6-4385-9d99-a8a1994bebac',
+        ).changeQueryDuration(8000)}
         onDownload={() => {}}
       />
     );
diff --git 
a/web-console/src/views/query-view/query-extra-info/query-extra-info.tsx 
b/web-console/src/views/query-view/query-extra-info/query-extra-info.tsx
index 7eae389..8a21546 100644
--- a/web-console/src/views/query-view/query-extra-info/query-extra-info.tsx
+++ b/web-console/src/views/query-view/query-extra-info/query-extra-info.tsx
@@ -28,6 +28,7 @@ import {
 } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
 import copy from 'copy-to-clipboard';
+import { QueryResult } from 'druid-query-toolkit';
 import React from 'react';
 
 import { AppToaster } from '../../../singletons/toaster';
@@ -35,25 +36,16 @@ import { pluralIfNeeded } from '../../../utils';
 
 import './query-extra-info.scss';
 
-export interface QueryExtraInfoData {
-  queryId?: string;
-  sqlQueryId?: string;
-  startTime: Date;
-  endTime: Date;
-  numResults: number;
-  wrapQueryLimit: number | undefined;
-}
-
 export interface QueryExtraInfoProps {
-  queryExtraInfo: QueryExtraInfoData;
+  queryResult: QueryResult;
   onDownload: (filename: string, format: string) => void;
 }
 
 export const QueryExtraInfo = React.memo(function QueryExtraInfo(props: 
QueryExtraInfoProps) {
-  const { queryExtraInfo, onDownload } = props;
+  const { queryResult, onDownload } = props;
 
   function handleQueryInfoClick() {
-    const id = queryExtraInfo.queryId || queryExtraInfo.sqlQueryId;
+    const id = queryResult.queryId || queryResult.sqlQueryId;
     if (!id) return;
 
     copy(id, { format: 'text/plain' });
@@ -64,7 +56,7 @@ export const QueryExtraInfo = React.memo(function 
QueryExtraInfo(props: QueryExt
   }
 
   function handleDownload(format: string) {
-    const id = queryExtraInfo.queryId || queryExtraInfo.sqlQueryId;
+    const id = queryResult.queryId || queryResult.sqlQueryId;
     if (!id) return;
 
     onDownload(`query-${id}.${format}`, format);
@@ -79,40 +71,38 @@ export const QueryExtraInfo = React.memo(function 
QueryExtraInfo(props: QueryExt
     </Menu>
   );
 
+  const wrapQueryLimit = queryResult.getSqlOuterLimit();
   let resultCount: string;
-  if (
-    queryExtraInfo.wrapQueryLimit &&
-    queryExtraInfo.numResults === queryExtraInfo.wrapQueryLimit
-  ) {
-    resultCount = `${queryExtraInfo.numResults - 1}+ results`;
+  if (wrapQueryLimit && queryResult.getNumResults() === wrapQueryLimit) {
+    resultCount = `${queryResult.getNumResults() - 1}+ results`;
   } else {
-    resultCount = pluralIfNeeded(queryExtraInfo.numResults, 'result');
+    resultCount = pluralIfNeeded(queryResult.getNumResults(), 'result');
   }
 
-  const elapsed = queryExtraInfo.endTime.valueOf() - 
queryExtraInfo.startTime.valueOf();
-
   let tooltipContent: JSX.Element | undefined;
-  if (queryExtraInfo.queryId) {
+  if (queryResult.queryId) {
     tooltipContent = (
       <>
-        Query ID: <strong>{queryExtraInfo.queryId}</strong> (click to copy)
+        Query ID: <strong>{queryResult.queryId}</strong> (click to copy)
       </>
     );
-  } else if (queryExtraInfo.sqlQueryId) {
+  } else if (queryResult.sqlQueryId) {
     tooltipContent = (
       <>
-        SQL query ID: <strong>{queryExtraInfo.sqlQueryId}</strong> (click to 
copy)
+        SQL query ID: <strong>{queryResult.sqlQueryId}</strong> (click to copy)
       </>
     );
   }
 
   return (
     <div className="query-extra-info">
-      <div className="query-info" onClick={handleQueryInfoClick}>
-        <Tooltip content={tooltipContent} hoverOpenDelay={500}>
-          {`${resultCount} in ${(elapsed / 1000).toFixed(2)}s`}
-        </Tooltip>
-      </div>
+      {typeof queryResult.queryDuration !== 'undefined' && (
+        <div className="query-info" onClick={handleQueryInfoClick}>
+          <Tooltip content={tooltipContent} hoverOpenDelay={500}>
+            {`${resultCount} in ${(queryResult.queryDuration / 
1000).toFixed(2)}s`}
+          </Tooltip>
+        </div>
+      )}
       <Popover className="download-button" content={downloadMenu} 
position={Position.BOTTOM_RIGHT}>
         <Button icon={IconNames.DOWNLOAD} minimal />
       </Popover>
diff --git a/web-console/src/views/query-view/query-input/query-input.tsx 
b/web-console/src/views/query-view/query-input/query-input.tsx
index a8b058c..a72f6b8 100644
--- a/web-console/src/views/query-view/query-input/query-input.tsx
+++ b/web-console/src/views/query-view/query-input/query-input.tsx
@@ -17,7 +17,7 @@
  */
 
 import { IResizeEntry, ResizeSensor } from '@blueprintjs/core';
-import ace from 'brace';
+import ace, { Editor } from 'brace';
 import escape from 'lodash.escape';
 import React from 'react';
 import AceEditor from 'react-ace';
@@ -29,7 +29,7 @@ import {
   SQL_KEYWORDS,
 } from '../../../../lib/keywords';
 import { SQL_DATA_TYPES, SQL_FUNCTIONS } from '../../../../lib/sql-docs';
-import { uniq } from '../../../utils';
+import { RowColumn, uniq } from '../../../utils';
 import { ColumnMetadata } from '../../../utils/column-metadata';
 
 import './query-input.scss';
@@ -56,6 +56,8 @@ export interface QueryInputState {
 }
 
 export class QueryInput extends React.PureComponent<QueryInputProps, 
QueryInputState> {
+  private aceEditor: Editor | undefined;
+
   static replaceDefaultAutoCompleter(): void {
     if (!langTools) return;
 
@@ -209,6 +211,13 @@ export class QueryInput extends 
React.PureComponent<QueryInputProps, QueryInputS
     onQueryStringChange(value);
   };
 
+  public goToRowColumn(rowColumn: RowColumn) {
+    const { aceEditor } = this;
+    if (!aceEditor) return;
+    aceEditor.getSelection().moveCursorTo(rowColumn.row, rowColumn.column);
+    aceEditor.focus(); // Grab the focus also
+  }
+
   render(): JSX.Element {
     const { queryString, runeMode } = this.props;
     const { editorHeight } = this.state;
@@ -240,6 +249,9 @@ export class QueryInput extends 
React.PureComponent<QueryInputProps, QueryInputS
               }}
               style={{}}
               placeholder="SELECT * FROM ..."
+              onLoad={(editor: any) => {
+                this.aceEditor = editor;
+              }}
             />
           </div>
         </ResizeSensor>
diff --git 
a/web-console/src/views/query-view/query-output/__snapshots__/query-output.spec.tsx.snap
 
b/web-console/src/views/query-view/query-output/__snapshots__/query-output.spec.tsx.snap
index 884542a..29d2ff2 100644
--- 
a/web-console/src/views/query-view/query-output/__snapshots__/query-output.spec.tsx.snap
+++ 
b/web-console/src/views/query-view/query-output/__snapshots__/query-output.spec.tsx.snap
@@ -38,6 +38,25 @@ exports[`query output matches snapshot 1`] = `
                     class=""
                   >
                     language
+                    <span
+                      class="bp3-icon bp3-icon-filter"
+                      icon="filter"
+                    >
+                      <svg
+                        data-icon="filter"
+                        height="14"
+                        viewBox="0 0 16 16"
+                        width="14"
+                      >
+                        <desc>
+                          filter
+                        </desc>
+                        <path
+                          d="M13.99.99h-12a1.003 1.003 0 00-.71 1.71l4.71 
4.71V14a1.003 1.003 0 001.71.71l2-2c.18-.18.29-.43.29-.71V7.41L14.7 2.7a1.003 
1.003 0 00-.71-1.71z"
+                          fill-rule="evenodd"
+                        />
+                      </svg>
+                    </span>
                   </div>
                 </span>
               </span>
@@ -65,6 +84,25 @@ exports[`query output matches snapshot 1`] = `
                     class=""
                   >
                     Count
+                    <span
+                      class="bp3-icon bp3-icon-filter"
+                      icon="filter"
+                    >
+                      <svg
+                        data-icon="filter"
+                        height="14"
+                        viewBox="0 0 16 16"
+                        width="14"
+                      >
+                        <desc>
+                          filter
+                        </desc>
+                        <path
+                          d="M13.99.99h-12a1.003 1.003 0 00-.71 1.71l4.71 
4.71V14a1.003 1.003 0 001.71.71l2-2c.18-.18.29-.43.29-.71V7.41L14.7 2.7a1.003 
1.003 0 00-.71-1.71z"
+                          fill-rule="evenodd"
+                        />
+                      </svg>
+                    </span>
                   </div>
                 </span>
               </span>
diff --git a/web-console/src/views/query-view/query-output/query-output.scss 
b/web-console/src/views/query-view/query-output/query-output.scss
index c8807a9..e90d2eb 100644
--- a/web-console/src/views/query-view/query-output/query-output.scss
+++ b/web-console/src/views/query-view/query-output/query-output.scss
@@ -39,12 +39,18 @@
     &.aggregate-header {
       background: rgb(75, 122, 148);
     }
+
     .asc {
       box-shadow: inset 0 3px 0 0 rgba(255, 255, 255, 0.6);
     }
+
     .desc {
       box-shadow: inset 0 -3px 0 0 rgba(255, 255, 255, 0.6);
     }
+
+    .bp3-icon {
+      margin-left: 3px;
+    }
   }
   .rt-td {
     cursor: pointer;
diff --git 
a/web-console/src/views/query-view/query-output/query-output.spec.tsx 
b/web-console/src/views/query-view/query-output/query-output.spec.tsx
index 6417b91..33adf69 100644
--- a/web-console/src/views/query-view/query-output/query-output.spec.tsx
+++ b/web-console/src/views/query-view/query-output/query-output.spec.tsx
@@ -17,14 +17,14 @@
  */
 
 import { render } from '@testing-library/react';
-import { parseSqlQuery } from 'druid-query-toolkit';
+import { QueryResult, SqlQuery } from 'druid-query-toolkit';
 import React from 'react';
 
 import { QueryOutput } from './query-output';
 
 describe('query output', () => {
   it('matches snapshot', () => {
-    const parsedQuery = parseSqlQuery(`SELECT
+    const parsedQuery = SqlQuery.parse(`SELECT
   "language",
   COUNT(*) AS "Count", COUNT(DISTINCT "language") AS "dist_language", COUNT(*) 
FILTER (WHERE "language"= 'xxx') AS "language_filtered_count"
 FROM "github"
@@ -38,17 +38,18 @@ ORDER BY "Count" DESC`);
         runeMode={false}
         loading={false}
         error="lol"
-        queryResult={{
-          header: ['language', 'Count', 'dist_language', 
'language_filtered_count'],
-          rows: [
+        queryResult={QueryResult.fromRawResult(
+          [
+            ['language', 'Count', 'dist_language', 'language_filtered_count'],
             ['', 6881, 1, 0],
             ['JavaScript', 166, 1, 0],
             ['Python', 62, 1, 0],
             ['HTML', 46, 1, 0],
             [],
           ],
-        }}
-        parsedQuery={parsedQuery}
+          false,
+          true,
+        ).attachQuery({}, parsedQuery)}
         onQueryChange={() => null}
       />
     );
diff --git a/web-console/src/views/query-view/query-output/query-output.tsx 
b/web-console/src/views/query-view/query-output/query-output.tsx
index c141c66..238fa6e 100644
--- a/web-console/src/views/query-view/query-output/query-output.tsx
+++ b/web-console/src/views/query-view/query-output/query-output.tsx
@@ -16,116 +16,163 @@
  * limitations under the License.
  */
 
-import { Menu, MenuItem, Popover } from '@blueprintjs/core';
-import { IconNames } from '@blueprintjs/icons';
-import { HeaderRows, SqlQuery } from 'druid-query-toolkit';
-import { basicIdentifierEscape, basicLiteralEscape } from 
'druid-query-toolkit/build/sql/helpers';
+import { Icon, Menu, MenuItem, Popover } from '@blueprintjs/core';
+import { IconName, IconNames } from '@blueprintjs/icons';
+import {
+  QueryResult,
+  SqlExpression,
+  SqlLiteral,
+  SqlQuery,
+  SqlRef,
+  trimString,
+} from 'druid-query-toolkit';
 import React, { useState } from 'react';
 import ReactTable from 'react-table';
 
 import { TableCell } from '../../../components';
 import { ShowValueDialog } from 
'../../../dialogs/show-value-dialog/show-value-dialog';
-import { copyAndAlert } from '../../../utils';
+import { copyAndAlert, prettyPrintSql } from '../../../utils';
 import { BasicAction, basicActionsToMenu } from '../../../utils/basic-action';
 
 import './query-output.scss';
 
-function trimValue(str: any): string {
-  str = String(str);
-  if (str.length < 102) return str;
-  return str.substr(0, 100) + '...';
+function isComparable(x: unknown): boolean {
+  return x !== null && x !== '' && !isNaN(Number(x));
 }
 
 export interface QueryOutputProps {
   loading: boolean;
-  queryResult?: HeaderRows;
-  parsedQuery?: SqlQuery;
+  queryResult?: QueryResult;
   onQueryChange: (query: SqlQuery, run?: boolean) => void;
   error?: string;
   runeMode: boolean;
 }
 
 export const QueryOutput = React.memo(function QueryOutput(props: 
QueryOutputProps) {
-  const { queryResult, parsedQuery, loading, error } = props;
+  const { queryResult, loading, error } = props;
+  const parsedQuery = queryResult ? queryResult.sqlQuery : undefined;
   const [showValue, setShowValue] = useState();
 
-  function getHeaderMenu(header: string) {
-    const { parsedQuery, onQueryChange, runeMode } = props;
+  function hasFilterOnHeader(header: string, headerIndex: number): boolean {
+    if (!parsedQuery || 
!parsedQuery.isRealOutputColumnAtSelectIndex(headerIndex)) return false;
+
+    return (
+      parsedQuery.getEffectiveWhereExpression().containsColumn(header) ||
+      parsedQuery.getEffectiveHavingExpression().containsColumn(header)
+    );
+  }
+
+  function getHeaderMenu(header: string, headerIndex: number) {
+    const { onQueryChange, runeMode } = props;
+    const ref = SqlRef.column(header);
+    const prettyRef = prettyPrintSql(ref);
 
     if (parsedQuery) {
-      const sorted = parsedQuery.getSorted();
+      const orderByExpression = parsedQuery.isValidSelectIndex(headerIndex)
+        ? SqlLiteral.index(headerIndex)
+        : SqlRef.column(header);
+      const descOrderBy = orderByExpression.toOrderByPart('DESC');
+      const ascOrderBy = orderByExpression.toOrderByPart('ASC');
+      const orderBy = parsedQuery.getOrderByForSelectIndex(headerIndex);
 
       const basicActions: BasicAction[] = [];
-      if (sorted) {
-        sorted.map(sorted => {
-          if (sorted.id === header) {
-            basicActions.push({
-              icon: sorted.desc ? IconNames.SORT_ASC : IconNames.SORT_DESC,
-              title: `Order by: ${trimValue(header)} ${sorted.desc ? 'ASC' : 
'DESC'}`,
-              onAction: () => {
-                onQueryChange(parsedQuery.orderBy(header, sorted.desc ? 'ASC' 
: 'DESC'), true);
-              },
-            });
-          }
+      if (orderBy) {
+        const reverseOrderBy = orderBy.reverseDirection();
+        const reverseOrderByDirection = reverseOrderBy.getEffectiveDirection();
+        basicActions.push({
+          icon: reverseOrderByDirection === 'ASC' ? IconNames.SORT_ASC : 
IconNames.SORT_DESC,
+          title: `Order ${reverseOrderByDirection === 'ASC' ? 'ascending' : 
'descending'}`,
+          onAction: () => {
+            
onQueryChange(parsedQuery.changeOrderByExpressions([reverseOrderBy]), true);
+          },
         });
-      }
-      if (!basicActions.length) {
+      } else {
         basicActions.push(
           {
             icon: IconNames.SORT_DESC,
-            title: `Order by: ${trimValue(header)} DESC`,
+            title: `Order descending`,
             onAction: () => {
-              onQueryChange(parsedQuery.orderBy(header, 'DESC'), true);
+              
onQueryChange(parsedQuery.changeOrderByExpressions([descOrderBy]), true);
             },
           },
           {
             icon: IconNames.SORT_ASC,
-            title: `Order by: ${trimValue(header)} ASC`,
+            title: `Order ascending`,
             onAction: () => {
-              onQueryChange(parsedQuery.orderBy(header, 'ASC'), true);
+              
onQueryChange(parsedQuery.changeOrderByExpressions([ascOrderBy]), true);
             },
           },
         );
       }
+
+      if (parsedQuery.isRealOutputColumnAtSelectIndex(headerIndex)) {
+        const whereExpression = parsedQuery.getWhereExpression();
+        if (whereExpression && whereExpression.containsColumn(header)) {
+          basicActions.push({
+            icon: IconNames.FILTER_REMOVE,
+            title: `Remove from WHERE clause`,
+            onAction: () => {
+              onQueryChange(
+                
parsedQuery.changeWhereExpression(whereExpression.removeColumnFromAnd(header)),
+                true,
+              );
+            },
+          });
+        }
+
+        const havingExpression = parsedQuery.getHavingExpression();
+        if (havingExpression && havingExpression.containsColumn(header)) {
+          basicActions.push({
+            icon: IconNames.FILTER_REMOVE,
+            title: `Remove from HAVING clause`,
+            onAction: () => {
+              onQueryChange(
+                
parsedQuery.changeHavingExpression(havingExpression.removeColumnFromAnd(header)),
+                true,
+              );
+            },
+          });
+        }
+      }
+
       basicActions.push({
         icon: IconNames.CROSS,
-        title: `Remove: ${trimValue(header)}`,
+        title: `Remove column`,
         onAction: () => {
-          onQueryChange(parsedQuery.remove(header), true);
+          onQueryChange(parsedQuery.removeOutputColumn(header), true);
         },
       });
 
       return basicActionsToMenu(basicActions);
     } else {
+      const orderByExpression = SqlRef.column(header);
+      const descOrderBy = orderByExpression.toOrderByPart('DESC');
+      const ascOrderBy = orderByExpression.toOrderByPart('ASC');
+      const descOrderByPretty = prettyPrintSql(descOrderBy);
+      const ascOrderByPretty = prettyPrintSql(descOrderBy);
       return (
         <Menu>
           <MenuItem
             icon={IconNames.CLIPBOARD}
-            text={`Copy: ${trimValue(header)}`}
+            text={`Copy: ${prettyRef}`}
             onClick={() => {
-              copyAndAlert(header, `${header}' copied to clipboard`);
+              copyAndAlert(String(ref), `${prettyRef}' copied to clipboard`);
             }}
           />
           {!runeMode && (
             <>
               <MenuItem
                 icon={IconNames.CLIPBOARD}
-                text={`Copy: ORDER BY ${basicIdentifierEscape(header)} ASC`}
+                text={`Copy: ${descOrderByPretty}`}
                 onClick={() =>
-                  copyAndAlert(
-                    `ORDER BY ${basicIdentifierEscape(header)} ASC`,
-                    `ORDER BY ${basicIdentifierEscape(header)} ASC' copied to 
clipboard`,
-                  )
+                  copyAndAlert(descOrderBy.toString(), `'${descOrderByPretty}' 
copied to clipboard`)
                 }
               />
               <MenuItem
                 icon={IconNames.CLIPBOARD}
-                text={`Copy: 'ORDER BY ${basicIdentifierEscape(header)} DESC'`}
+                text={`Copy: ${ascOrderByPretty}`}
                 onClick={() =>
-                  copyAndAlert(
-                    `ORDER BY ${basicIdentifierEscape(header)} DESC`,
-                    `ORDER BY ${basicIdentifierEscape(header)} DESC' copied to 
clipboard`,
-                  )
+                  copyAndAlert(ascOrderBy.toString(), `'${ascOrderByPretty}' 
copied to clipboard`)
                 }
               />
             </>
@@ -135,8 +182,37 @@ export const QueryOutput = React.memo(function 
QueryOutput(props: QueryOutputPro
     }
   }
 
-  function getCellMenu(header: string, value: any) {
-    const { parsedQuery, onQueryChange, runeMode } = props;
+  function filterOnMenuItem(icon: IconName, clause: SqlExpression, having: 
boolean) {
+    const { onQueryChange } = props;
+    if (!parsedQuery) return;
+
+    return (
+      <MenuItem
+        icon={icon}
+        text={`${having ? 'Having' : 'Filter on'}: ${prettyPrintSql(clause)}`}
+        onClick={() => {
+          onQueryChange(
+            having ? parsedQuery.addToHaving(clause) : 
parsedQuery.addToWhere(clause),
+            true,
+          );
+        }}
+      />
+    );
+  }
+
+  function clipboardMenuItem(clause: SqlExpression) {
+    const prettyLabel = prettyPrintSql(clause);
+    return (
+      <MenuItem
+        icon={IconNames.CLIPBOARD}
+        text={`Copy: ${prettyLabel}`}
+        onClick={() => copyAndAlert(clause.toString(), `${prettyLabel} copied 
to clipboard`)}
+      />
+    );
+  }
+
+  function getCellMenu(header: string, headerIndex: number, value: any) {
+    const { runeMode } = props;
 
     const showFullValueMenuItem =
       typeof value === 'string' ? (
@@ -151,117 +227,75 @@ export const QueryOutput = React.memo(function 
QueryOutput(props: QueryOutputPro
         undefined
       );
 
+    const val = SqlLiteral.create(value);
     if (parsedQuery) {
-      return (
-        <Menu>
-          <MenuItem
-            icon={IconNames.FILTER_KEEP}
-            text={`Filter by: ${trimValue(header)} = ${trimValue(value)}`}
-            onClick={() => {
-              onQueryChange(parsedQuery.addWhereFilter(header, '=', value), 
true);
-            }}
-          />
-          <MenuItem
-            icon={IconNames.FILTER_REMOVE}
-            text={`Filter by: ${trimValue(header)} != ${trimValue(value)}`}
-            onClick={() => {
-              onQueryChange(parsedQuery.addWhereFilter(header, '!=', value), 
true);
-            }}
-          />
-          {!isNaN(Number(value)) && (
-            <>
-              <MenuItem
-                icon={IconNames.FILTER_KEEP}
-                text={`Filter by: ${trimValue(header)} >= ${trimValue(value)}`}
-                onClick={() => {
-                  onQueryChange(parsedQuery.addWhereFilter(header, '>=', 
value), true);
-                }}
-              />
-              <MenuItem
-                icon={IconNames.FILTER_KEEP}
-                text={`Filter by: ${trimValue(header)} <= ${trimValue(value)}`}
-                onClick={() => {
-                  onQueryChange(parsedQuery.addWhereFilter(header, '<=', 
value), true);
-                }}
-              />
-            </>
-          )}
-          {showFullValueMenuItem}
-        </Menu>
-      );
-    } else {
-      return (
-        <Menu>
-          <MenuItem
-            icon={IconNames.CLIPBOARD}
-            text={`Copy: ${trimValue(value)}`}
-            onClick={() => copyAndAlert(value, `${value} copied to clipboard`)}
-          />
-          {!runeMode && (
-            <>
-              <MenuItem
-                icon={IconNames.CLIPBOARD}
-                text={`Copy: ${basicIdentifierEscape(header)} = 
${basicLiteralEscape(value)}`}
-                onClick={() =>
-                  copyAndAlert(
-                    `${basicIdentifierEscape(header)} = 
${basicLiteralEscape(value)}`,
-                    `${basicIdentifierEscape(header)} = ${basicLiteralEscape(
-                      value,
-                    )} copied to clipboard`,
-                  )
-                }
-              />
-              <MenuItem
-                icon={IconNames.CLIPBOARD}
-                text={`Copy: ${basicIdentifierEscape(header)} != 
${basicLiteralEscape(value)}`}
-                onClick={() =>
-                  copyAndAlert(
-                    `${basicIdentifierEscape(header)} != 
${basicLiteralEscape(value)}`,
-                    `${basicIdentifierEscape(header)} != ${basicLiteralEscape(
-                      value,
-                    )} copied to clipboard`,
-                  )
-                }
-              />
-            </>
-          )}
-          {showFullValueMenuItem}
-        </Menu>
-      );
+      const selectValue = parsedQuery.getSelectExpressionForIndex(headerIndex);
+      if (selectValue) {
+        const outputName = selectValue.getOutputName();
+        const having = parsedQuery.isAggregateSelectIndex(headerIndex);
+        let ex: SqlExpression;
+        if (having && outputName) {
+          ex = SqlRef.column(outputName);
+        } else {
+          ex = selectValue.expression as SqlExpression;
+        }
+
+        return (
+          <Menu>
+            {isComparable(value) && (
+              <>
+                {filterOnMenuItem(IconNames.FILTER_KEEP, 
ex.greaterThanOrEqual(val), having)}
+                {filterOnMenuItem(IconNames.FILTER_KEEP, 
ex.lessThanOrEqual(val), having)}
+              </>
+            )}
+            {filterOnMenuItem(IconNames.FILTER_KEEP, ex.equal(val), having)}
+            {filterOnMenuItem(IconNames.FILTER_REMOVE, ex.unequal(val), 
having)}
+            {showFullValueMenuItem}
+          </Menu>
+        );
+      }
     }
+
+    const ref = SqlRef.column(header);
+    const trimmedValue = trimString(String(value), 50);
+    return (
+      <Menu>
+        <MenuItem
+          icon={IconNames.CLIPBOARD}
+          text={`Copy: ${trimmedValue}`}
+          onClick={() => copyAndAlert(value, `${trimmedValue} copied to 
clipboard`)}
+        />
+        {!runeMode && (
+          <>
+            {clipboardMenuItem(ref.equal(val))}
+            {clipboardMenuItem(ref.unequal(val))}
+          </>
+        )}
+        {showFullValueMenuItem}
+      </Menu>
+    );
   }
 
   function getHeaderClassName(header: string) {
-    const { parsedQuery } = props;
     if (!parsedQuery) return;
 
     const className = [];
-    const sorted = parsedQuery.getSorted();
-    const aggregateColumns = parsedQuery.getAggregateColumns();
-
-    if (sorted) {
-      const sortedColumnNames = sorted.map(column => column.id);
-      if (sortedColumnNames.includes(header)) {
-        className.push(sorted[sortedColumnNames.indexOf(header)].desc ? 
'-sort-desc' : '-sort-asc');
-      }
+    const orderBy = parsedQuery.getOrderByForOutputColumn(header);
+    if (orderBy) {
+      className.push(orderBy.getEffectiveDirection() === 'DESC' ? '-sort-desc' 
: '-sort-asc');
     }
 
-    if (aggregateColumns && aggregateColumns.includes(header)) {
+    if (parsedQuery.isAggregateOutputColumn(header)) {
       className.push('aggregate-header');
     }
 
     return className.join(' ');
   }
 
-  let aggregateColumns: string[] | undefined;
-  if (parsedQuery) {
-    aggregateColumns = parsedQuery.getAggregateColumns();
-  }
-
   return (
     <div className="query-output">
       <ReactTable
-        data={queryResult ? queryResult.rows : []}
+        data={queryResult ? (queryResult.rows as any[][]) : []}
         loading={loading}
         noDataText={
           !loading && queryResult && !queryResult.rows.length
@@ -269,12 +303,16 @@ export const QueryOutput = React.memo(function 
QueryOutput(props: QueryOutputPro
             : error || ''
         }
         sortable={false}
-        columns={(queryResult ? queryResult.header : []).map((h: any, i) => {
+        columns={(queryResult ? queryResult.header : []).map((column, i) => {
+          const h = column.name;
           return {
             Header: () => {
               return (
-                <Popover className={'clickable-cell'} 
content={getHeaderMenu(h)}>
-                  <div>{h}</div>
+                <Popover className={'clickable-cell'} 
content={getHeaderMenu(h, i)}>
+                  <div>
+                    {h}
+                    {hasFilterOnHeader(h, i) && <Icon icon={IconNames.FILTER} 
iconSize={14} />}
+                  </div>
                 </Popover>
               );
             },
@@ -284,14 +322,16 @@ export const QueryOutput = React.memo(function 
QueryOutput(props: QueryOutputPro
               const value = row.value;
               return (
                 <div>
-                  <Popover content={getCellMenu(h, value)}>
+                  <Popover content={getCellMenu(h, i, value)}>
                     <TableCell value={value} unlimited />
                   </Popover>
                 </div>
               );
             },
             className:
-              aggregateColumns && aggregateColumns.includes(h) ? 
'aggregate-column' : undefined,
+              parsedQuery && parsedQuery.isAggregateOutputColumn(h)
+                ? 'aggregate-column'
+                : undefined,
           };
         })}
       />
diff --git a/web-console/src/views/query-view/query-view.tsx 
b/web-console/src/views/query-view/query-view.tsx
index 4e9a18c..d5a531a 100644
--- a/web-console/src/views/query-view/query-view.tsx
+++ b/web-console/src/views/query-view/query-view.tsx
@@ -19,17 +19,10 @@
 import { Intent, Switch, Tooltip } from '@blueprintjs/core';
 import axios from 'axios';
 import classNames from 'classnames';
-import {
-  HeaderRows,
-  isFirstRowHeader,
-  normalizeQueryResult,
-  parseSqlQuery,
-  shouldIncludeTimestamp,
-  SqlQuery,
-} from 'druid-query-toolkit';
+import { QueryResult, QueryRunner, SqlQuery } from 'druid-query-toolkit';
 import Hjson from 'hjson';
 import memoizeOne from 'memoize-one';
-import React from 'react';
+import React, { RefObject } from 'react';
 import SplitterLayout from 'react-splitter-layout';
 
 import { QueryPlanDialog } from '../../dialogs';
@@ -39,6 +32,7 @@ import { AppToaster } from '../../singletons/toaster';
 import {
   BasicQueryExplanation,
   downloadFile,
+  findEmptyLiteralPosition,
   getDruidErrorMessage,
   localStorageGet,
   localStorageGetJson,
@@ -55,7 +49,7 @@ import { isEmptyContext, QueryContext } from 
'../../utils/query-context';
 import { QueryRecord, QueryRecordUtil } from '../../utils/query-history';
 
 import { ColumnTree } from './column-tree/column-tree';
-import { QueryExtraInfo, QueryExtraInfoData } from 
'./query-extra-info/query-extra-info';
+import { QueryExtraInfo } from './query-extra-info/query-extra-info';
 import { QueryInput } from './query-input/query-input';
 import { QueryOutput } from './query-output/query-output';
 import { RunButton } from './run-button/run-button';
@@ -64,7 +58,7 @@ import './query-view.scss';
 
 const parser = memoizeOne((sql: string): SqlQuery | undefined => {
   try {
-    return parseSqlQuery(sql);
+    return SqlQuery.parse(sql);
   } catch {
     return;
   }
@@ -94,7 +88,7 @@ export interface QueryViewState {
   columnMetadataError?: string;
 
   loading: boolean;
-  result?: QueryResult;
+  queryResult?: QueryResult;
   error?: string;
 
   explainDialogOpen: boolean;
@@ -110,12 +104,6 @@ export interface QueryViewState {
   queryHistory: readonly QueryRecord[];
 }
 
-interface QueryResult {
-  queryResult: HeaderRows;
-  queryExtraInfo: QueryExtraInfoData;
-  parsedQuery?: SqlQuery;
-}
-
 export class QueryView extends React.PureComponent<QueryViewProps, 
QueryViewState> {
   static trimSemicolon(query: string): string {
     // Trims out a trailing semicolon while preserving space 
(https://bit.ly/1n1yfkJ)
@@ -166,16 +154,20 @@ export class QueryView extends 
React.PureComponent<QueryViewProps, QueryViewStat
   }
 
   private metadataQueryManager: QueryManager<null, ColumnMetadata[]>;
-  private sqlQueryManager: QueryManager<QueryWithContext, QueryResult>;
+  private queryManager: QueryManager<QueryWithContext, QueryResult>;
   private explainQueryManager: QueryManager<
     QueryWithContext,
     BasicQueryExplanation | SemiJoinQueryExplanation | string
   >;
 
+  private queryInputRef: RefObject<QueryInput>;
+
   constructor(props: QueryViewProps, context: any) {
     super(props, context);
     const { mandatoryQueryContext } = props;
 
+    this.queryInputRef = React.createRef();
+
     const queryString = props.initQuery || 
localStorageGet(LocalStorageKeys.QUERY_KEY) || '';
     const parsedQuery = queryString ? parser(queryString) : undefined;
 
@@ -228,86 +220,33 @@ export class QueryView extends 
React.PureComponent<QueryViewProps, QueryViewStat
       },
     });
 
-    this.sqlQueryManager = new QueryManager({
+    const queryRunner = new QueryRunner((payload, isSql) => {
+      return axios.post(`/druid/v2${isSql ? '/sql' : ''}`, payload);
+    });
+
+    this.queryManager = new QueryManager({
       processQuery: async (queryWithContext: QueryWithContext): 
Promise<QueryResult> => {
         const { queryString, queryContext, wrapQueryLimit } = queryWithContext;
 
-        let parsedQuery: SqlQuery | undefined;
-        let jsonQuery: any;
-
-        try {
-          parsedQuery = parser(queryString);
-        } catch {}
-
-        if (!(parsedQuery instanceof SqlQuery)) {
-          parsedQuery = undefined;
-        }
-        if (QueryView.isJsonLike(queryString)) {
-          jsonQuery = Hjson.parse(queryString);
-        } else {
-          jsonQuery = {
-            query: queryString,
-            resultFormat: 'array',
-            header: true,
-          };
-        }
+        const query = QueryView.isJsonLike(queryString) ? 
Hjson.parse(queryString) : queryString;
 
+        let context: Record<string, any> | undefined;
         if (!isEmptyContext(queryContext) || wrapQueryLimit || 
mandatoryQueryContext) {
-          jsonQuery.context = Object.assign(
-            {},
-            jsonQuery.context || {},
-            queryContext,
-            mandatoryQueryContext || {},
-          );
-          jsonQuery.context.sqlOuterLimit = wrapQueryLimit;
-        }
-
-        let rawQueryResult: unknown;
-        let queryId: string | undefined;
-        let sqlQueryId: string | undefined;
-        const startTime = new Date();
-        let endTime: Date;
-        if (!jsonQuery.queryType && typeof jsonQuery.query === 'string') {
-          try {
-            const sqlResultResp = await axios.post('/druid/v2/sql', jsonQuery);
-            endTime = new Date();
-            rawQueryResult = sqlResultResp.data;
-            sqlQueryId = sqlResultResp.headers['x-druid-sql-query-id'];
-          } catch (e) {
-            throw new Error(getDruidErrorMessage(e));
-          }
-        } else {
-          try {
-            const runeResultResp = await axios.post('/druid/v2', jsonQuery);
-            endTime = new Date();
-            rawQueryResult = runeResultResp.data;
-            queryId = runeResultResp.headers['x-druid-query-id'];
-          } catch (e) {
-            throw new Error(getDruidErrorMessage(e));
+          context = Object.assign({}, queryContext, mandatoryQueryContext || 
{});
+          if (typeof wrapQueryLimit !== 'undefined') {
+            context.sqlOuterLimit = wrapQueryLimit;
           }
         }
 
-        const queryResult = normalizeQueryResult(
-          rawQueryResult,
-          shouldIncludeTimestamp(jsonQuery),
-          isFirstRowHeader(jsonQuery),
-        );
-        return {
-          queryResult,
-          queryExtraInfo: {
-            queryId,
-            sqlQueryId,
-            startTime,
-            endTime,
-            numResults: queryResult.rows.length,
-            wrapQueryLimit,
-          },
-          parsedQuery,
-        };
+        try {
+          return await queryRunner.runQuery(query, context);
+        } catch (e) {
+          throw new Error(getDruidErrorMessage(e));
+        }
       },
       onStateChange: ({ result, loading, error }) => {
         this.setState({
-          result,
+          queryResult: result,
           loading,
           error,
         });
@@ -323,9 +262,15 @@ export class QueryView extends 
React.PureComponent<QueryViewProps, QueryViewStat
           resultFormat: 'object',
         };
 
-        if (!isEmptyContext(queryContext) || wrapQueryLimit) {
-          explainPayload.context = Object.assign({}, queryContext || {});
-          explainPayload.context.sqlOuterLimit = wrapQueryLimit;
+        if (!isEmptyContext(queryContext) || wrapQueryLimit || 
mandatoryQueryContext) {
+          explainPayload.context = Object.assign(
+            {},
+            queryContext || {},
+            mandatoryQueryContext || {},
+          );
+          if (typeof wrapQueryLimit !== 'undefined') {
+            explainPayload.context.sqlOuterLimit = wrapQueryLimit;
+          }
         }
         const result = await queryDruidSql(explainPayload);
 
@@ -347,27 +292,36 @@ export class QueryView extends 
React.PureComponent<QueryViewProps, QueryViewStat
 
   componentWillUnmount(): void {
     this.metadataQueryManager.terminate();
-    this.sqlQueryManager.terminate();
+    this.queryManager.terminate();
     this.explainQueryManager.terminate();
   }
 
   prettyPrintJson(): void {
-    this.setState(prevState => ({
-      queryString: JSON.stringify(Hjson.parse(prevState.queryString), null, 2),
-    }));
+    this.setState(prevState => {
+      let parsed: any;
+      try {
+        parsed = Hjson.parse(prevState.queryString);
+      } catch {
+        return null;
+      }
+      return {
+        queryString: JSON.stringify(parsed, null, 2),
+      };
+    });
   }
 
   handleDownload = (filename: string, format: string) => {
-    const { result } = this.state;
-    if (!result) return;
-    const { queryResult } = result;
+    const { queryResult } = this.state;
+    if (!queryResult) return;
 
     let lines: string[] = [];
     let separator: string = '';
 
     if (format === 'csv' || format === 'tsv') {
       separator = format === 'csv' ? ',' : '\t';
-      lines.push(queryResult.header.map(str => QueryView.formatStr(str, 
format)).join(separator));
+      lines.push(
+        queryResult.header.map(column => QueryView.formatStr(column.name, 
format)).join(separator),
+      );
       lines = lines.concat(
         queryResult.rows.map(r => r.map(cell => QueryView.formatStr(cell, 
format)).join(separator)),
       );
@@ -378,7 +332,7 @@ export class QueryView extends 
React.PureComponent<QueryViewProps, QueryViewStat
         for (let k = 0; k < r.length; k++) {
           const newName = queryResult.header[k];
           if (newName) {
-            outputObject[newName] = r[k];
+            outputObject[newName.name] = r[k];
           }
         }
         return JSON.stringify(outputObject);
@@ -473,15 +427,15 @@ export class QueryView extends 
React.PureComponent<QueryViewProps, QueryViewStat
   }
 
   renderMainArea() {
-    const { queryString, queryContext, loading, result, error, columnMetadata 
} = this.state;
+    const { queryString, queryContext, loading, queryResult, error, 
columnMetadata } = this.state;
     const emptyQuery = QueryView.isEmptyQuery(queryString);
 
     let currentSchema: string | undefined;
     let currentTable: string | undefined;
 
-    if (result && result.parsedQuery) {
-      currentSchema = result.parsedQuery.getSchema();
-      currentTable = result.parsedQuery.getTableName();
+    if (queryResult && queryResult.sqlQuery) {
+      currentSchema = queryResult.sqlQuery.getFirstSchema();
+      currentTable = queryResult.sqlQuery.getFirstTableName();
     } else if (localStorageGet(LocalStorageKeys.QUERY_KEY)) {
       const defaultQueryString = localStorageGet(LocalStorageKeys.QUERY_KEY);
 
@@ -490,8 +444,8 @@ export class QueryView extends 
React.PureComponent<QueryViewProps, QueryViewStat
         : undefined;
 
       if (defaultQueryAst) {
-        currentSchema = defaultQueryAst.getSchema();
-        currentTable = defaultQueryAst.getTableName();
+        currentSchema = defaultQueryAst.getFirstSchema();
+        currentTable = defaultQueryAst.getFirstTableName();
       }
     }
 
@@ -509,6 +463,7 @@ export class QueryView extends 
React.PureComponent<QueryViewProps, QueryViewStat
       >
         <div className="control-pane">
           <QueryInput
+            ref={this.queryInputRef}
             currentSchema={currentSchema ? currentSchema : 'druid'}
             currentTable={currentTable}
             queryString={queryString}
@@ -530,11 +485,8 @@ export class QueryView extends 
React.PureComponent<QueryViewProps, QueryViewStat
             />
             {this.renderAutoRunSwitch()}
             {this.renderWrapQueryLimitSelector()}
-            {result && (
-              <QueryExtraInfo
-                queryExtraInfo={result.queryExtraInfo}
-                onDownload={this.handleDownload}
-              />
+            {queryResult && (
+              <QueryExtraInfo queryResult={queryResult} 
onDownload={this.handleDownload} />
             )}
           </div>
         </div>
@@ -542,19 +494,30 @@ export class QueryView extends 
React.PureComponent<QueryViewProps, QueryViewStat
           runeMode={runeMode}
           loading={loading}
           error={error}
-          queryResult={result ? result.queryResult : undefined}
-          parsedQuery={result ? result.parsedQuery : undefined}
-          onQueryChange={this.handleQueryStringChange}
+          queryResult={queryResult}
+          onQueryChange={this.handleQueryChange}
         />
       </SplitterLayout>
     );
   }
 
-  private handleQueryStringChange = (
-    queryString: string | SqlQuery,
-    preferablyRun?: boolean,
-  ): void => {
-    if (queryString instanceof SqlQuery) queryString = queryString.toString();
+  private handleQueryChange = (query: SqlQuery, preferablyRun?: boolean): void 
=> {
+    this.handleQueryStringChange(query.toString(), preferablyRun);
+
+    // Possibly move the cursor of the QueryInput to the empty literal position
+    const emptyLiteralPosition = findEmptyLiteralPosition(query);
+    if (emptyLiteralPosition) {
+      // Introduce a delay to let the new text appear
+      setTimeout(() => {
+        const currentQueryInput = this.queryInputRef.current;
+        if (currentQueryInput) {
+          currentQueryInput.goToRowColumn(emptyLiteralPosition);
+        }
+      }, 10);
+    }
+  };
+
+  private handleQueryStringChange = (queryString: string, preferablyRun?: 
boolean): void => {
     this.setState({ queryString, parsedQuery: parser(queryString) }, () => {
       const { autoRun } = this.state;
       if (preferablyRun && autoRun) this.handleRun();
@@ -589,7 +552,7 @@ export class QueryView extends 
React.PureComponent<QueryViewProps, QueryViewStat
     localStorageSetJson(LocalStorageKeys.QUERY_CONTEXT, queryContext);
 
     this.setState({ queryHistory: newQueryHistory });
-    this.sqlQueryManager.runQuery({ queryString, queryContext, wrapQueryLimit 
});
+    this.queryManager.runQuery({ queryString, queryContext, wrapQueryLimit });
   };
 
   private handleExplain = () => {
@@ -614,8 +577,8 @@ export class QueryView extends 
React.PureComponent<QueryViewProps, QueryViewStat
     let defaultSchema;
     let defaultTable;
     if (parsedQuery instanceof SqlQuery) {
-      defaultSchema = parsedQuery.getSchema();
-      defaultTable = parsedQuery.getTableName();
+      defaultSchema = parsedQuery.getFirstSchema();
+      defaultTable = parsedQuery.getFirstTableName();
     }
 
     return (
@@ -627,7 +590,7 @@ export class QueryView extends 
React.PureComponent<QueryViewProps, QueryViewStat
             getParsedQuery={this.getParsedQuery}
             columnMetadataLoading={columnMetadataLoading}
             columnMetadata={columnMetadata}
-            onQueryStringChange={this.handleQueryStringChange}
+            onQueryChange={this.handleQueryChange}
             defaultSchema={defaultSchema ? defaultSchema : 'druid'}
             defaultTable={defaultTable}
           />


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to