Просмотр исходного кода

HUE-8856 [autocomplete] Use a dedicated sqlParseSupport.js for each dialect

Johan Ahlen 6 лет назад
Родитель
Сommit
117a0cd6fe
17 измененных файлов с 7491 добавлено и 113 удалено
  1. 0 91
      desktop/core/src/desktop/js/parse/impalaAutocompleteParser.js
  2. 1 1
      desktop/core/src/desktop/js/parse/sql/generic/genericAutocompleteParser.js
  3. 1 1
      desktop/core/src/desktop/js/parse/sql/generic/genericSyntaxParser.js
  4. 2 2
      desktop/core/src/desktop/js/parse/sql/generic/spec/genericAutocompleteParser_Alter_Spec.js
  5. 3 3
      desktop/core/src/desktop/js/parse/sql/generic/spec/genericAutocompleteParser_Create_Spec.js
  6. 2 2
      desktop/core/src/desktop/js/parse/sql/generic/spec/genericAutocompleteParser_Drop_Spec.js
  7. 2 2
      desktop/core/src/desktop/js/parse/sql/generic/spec/genericAutocompleteParser_Error_Spec.js
  8. 2 2
      desktop/core/src/desktop/js/parse/sql/generic/spec/genericAutocompleteParser_Insert_Spec.js
  9. 3 3
      desktop/core/src/desktop/js/parse/sql/generic/spec/genericAutocompleteParser_Locations_Spec.js
  10. 2277 0
      desktop/core/src/desktop/js/parse/sql/generic/sqlParseSupport.js
  11. 1 1
      desktop/core/src/desktop/js/parse/sql/hive/hiveAutocompleteParser.js
  12. 1 1
      desktop/core/src/desktop/js/parse/sql/hive/hiveSyntaxParser.js
  13. 2505 0
      desktop/core/src/desktop/js/parse/sql/hive/sqlParseSupport.js
  14. 91 0
      desktop/core/src/desktop/js/parse/sql/impala/impalaAutocompleteParser.js
  15. 1 1
      desktop/core/src/desktop/js/parse/sql/impala/impalaSyntaxParser.js
  16. 2590 0
      desktop/core/src/desktop/js/parse/sql/impala/sqlParseSupport.js
  17. 9 3
      tools/jison/generateParsers.js

Разница между файлами не показана из-за своего большого размера
+ 0 - 91
desktop/core/src/desktop/js/parse/impalaAutocompleteParser.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
desktop/core/src/desktop/js/parse/sql/generic/genericAutocompleteParser.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
desktop/core/src/desktop/js/parse/sql/generic/genericSyntaxParser.js


+ 2 - 2
desktop/core/src/desktop/js/parse/spec/generic/genericAutocompleteParser_Alter_Spec.js → desktop/core/src/desktop/js/parse/sql/generic/spec/genericAutocompleteParser_Alter_Spec.js

@@ -14,8 +14,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import SqlTestUtils from '../sqlTestUtils';
-import genericAutocompleteParser from '../../genericAutocompleteParser';
+import SqlTestUtils from 'parse/spec/sqlTestUtils';
+import genericAutocompleteParser from '../genericAutocompleteParser';
 
 describe('genericAutocompleteParser.js ALTER statements', () => {
   beforeAll(() => {

+ 3 - 3
desktop/core/src/desktop/js/parse/spec/generic/genericAutocompleteParser_Create_Spec.js → desktop/core/src/desktop/js/parse/sql/generic/spec/genericAutocompleteParser_Create_Spec.js

@@ -14,10 +14,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import SqlTestUtils from '../sqlTestUtils';
-import genericAutocompleteParser from '../../genericAutocompleteParser';
+import SqlTestUtils from 'parse/spec/sqlTestUtils';
+import genericAutocompleteParser from '../genericAutocompleteParser';
 
-describe('genericAutocompleteParser.js CREATE statements', () => {
+fdescribe('genericAutocompleteParser.js CREATE statements', () => {
   beforeAll(() => {
     genericAutocompleteParser.yy.parseError = function(msg) {
       throw Error(msg);

+ 2 - 2
desktop/core/src/desktop/js/parse/spec/generic/genericAutocompleteParser_Drop_Spec.js → desktop/core/src/desktop/js/parse/sql/generic/spec/genericAutocompleteParser_Drop_Spec.js

@@ -14,8 +14,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import SqlTestUtils from '../sqlTestUtils';
-import genericAutocompleteParser from '../../genericAutocompleteParser';
+import SqlTestUtils from 'parse/spec/sqlTestUtils';
+import genericAutocompleteParser from '../genericAutocompleteParser';
 
 describe('genericAutocompleteParser.js DROP statements', () => {
   beforeAll(() => {

+ 2 - 2
desktop/core/src/desktop/js/parse/spec/generic/genericAutocompleteParser_Error_Spec.js → desktop/core/src/desktop/js/parse/sql/generic/spec/genericAutocompleteParser_Error_Spec.js

@@ -14,8 +14,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import SqlTestUtils from '../sqlTestUtils';
-import genericAutocompleteParser from '../../genericAutocompleteParser';
+import SqlTestUtils from 'parse/spec/sqlTestUtils';
+import genericAutocompleteParser from '../genericAutocompleteParser';
 
 describe('genericAutocompleteParser.js Error statements', () => {
   beforeAll(() => {

+ 2 - 2
desktop/core/src/desktop/js/parse/spec/generic/genericAutocompleteParser_Insert_Spec.js → desktop/core/src/desktop/js/parse/sql/generic/spec/genericAutocompleteParser_Insert_Spec.js

@@ -14,8 +14,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import SqlTestUtils from '../sqlTestUtils';
-import genericAutocompleteParser from '../../genericAutocompleteParser';
+import SqlTestUtils from 'parse/spec/sqlTestUtils';
+import genericAutocompleteParser from '../genericAutocompleteParser';
 
 describe('genericAutocompleteParser.js INSERT statements', () => {
   beforeAll(() => {

+ 3 - 3
desktop/core/src/desktop/js/parse/spec/generic/genericAutocompleteParser_Locations_Spec.js → desktop/core/src/desktop/js/parse/sql/generic/spec/genericAutocompleteParser_Locations_Spec.js

@@ -14,10 +14,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import SqlTestUtils from '../sqlTestUtils';
-import genericAutocompleteParser from '../../genericAutocompleteParser';
+import SqlTestUtils from 'parse/spec/sqlTestUtils';
+import genericAutocompleteParser from '../genericAutocompleteParser';
 
-describe('sqlAutocompleteParser.js locations', () => {
+describe('genericAutocompleteParser.js locations', () => {
   beforeAll(() => {
     genericAutocompleteParser.yy.parseError = function(msg) {
       throw Error(msg);

+ 2277 - 0
desktop/core/src/desktop/js/parse/sql/generic/sqlParseSupport.js

@@ -0,0 +1,2277 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 { SqlFunctions } from 'sql/sqlFunctions';
+
+const identifierEquals = (a, b) =>
+  a &&
+  b &&
+  a
+    .replace(/^\s*`/, '')
+    .replace(/`\s*$/, '')
+    .toLowerCase() ===
+    b
+      .replace(/^\s*`/, '')
+      .replace(/`\s*$/, '')
+      .toLowerCase();
+
+// endsWith polyfill from hue_utils.js, needed as workers live in their own js environment
+if (!String.prototype.endsWith) {
+  String.prototype.endsWith = function(searchString, position) {
+    const subjectString = this.toString();
+    if (
+      typeof position !== 'number' ||
+      !isFinite(position) ||
+      Math.floor(position) !== position ||
+      position > subjectString.length
+    ) {
+      position = subjectString.length;
+    }
+    position -= searchString.length;
+    const lastIndex = subjectString.lastIndexOf(searchString, position);
+    return lastIndex !== -1 && lastIndex === position;
+  };
+}
+
+/**
+ * Calculates the Optimal String Alignment distance between two strings. Returns 0 when the strings are equal and the
+ * distance when not, distances is less than or equal to the length of the longest string.
+ *
+ * @param strA
+ * @param strB
+ * @param [ignoreCase]
+ * @returns {number} The similarity
+ */
+const stringDistance = function(strA, strB, ignoreCase) {
+  if (ignoreCase) {
+    strA = strA.toLowerCase();
+    strB = strB.toLowerCase();
+  }
+
+  // TODO: Consider other algorithms for performance
+  const strALength = strA.length;
+  const strBLength = strB.length;
+  if (strALength === 0) {
+    return strBLength;
+  }
+  if (strBLength === 0) {
+    return strALength;
+  }
+
+  const distances = new Array(strALength);
+
+  let cost, deletion, insertion, substitution, transposition;
+  for (let i = 0; i <= strALength; i++) {
+    distances[i] = new Array(strBLength);
+    distances[i][0] = i;
+    for (let j = 1; j <= strBLength; j++) {
+      if (!i) {
+        distances[0][j] = j;
+      } else {
+        cost = strA[i - 1] === strB[j - 1] ? 0 : 1;
+        deletion = distances[i - 1][j] + 1;
+        insertion = distances[i][j - 1] + 1;
+        substitution = distances[i - 1][j - 1] + cost;
+        if (deletion <= insertion && deletion <= substitution) {
+          distances[i][j] = deletion;
+        } else if (insertion <= deletion && insertion <= substitution) {
+          distances[i][j] = insertion;
+        } else {
+          distances[i][j] = substitution;
+        }
+
+        if (i > 1 && j > 1 && strA[i] === strB[j - 1] && strA[i - 1] === strB[j]) {
+          transposition = distances[i - 2][j - 2] + cost;
+          if (transposition < distances[i][j]) {
+            distances[i][j] = transposition;
+          }
+        }
+      }
+    }
+  }
+
+  return distances[strALength][strBLength];
+};
+
+const equalIgnoreCase = (a, b) => a && b && a.toLowerCase() === b.toLowerCase();
+
+const SIMPLE_TABLE_REF_SUGGESTIONS = [
+  'suggestJoinConditions',
+  'suggestAggregateFunctions',
+  'suggestFilters',
+  'suggestGroupBys',
+  'suggestOrderBys'
+];
+
+const initSqlParser = function(parser) {
+  parser.prepareNewStatement = function() {
+    linkTablePrimaries();
+    parser.commitLocations();
+
+    delete parser.yy.latestCommonTableExpressions;
+    delete parser.yy.correlatedSubQuery;
+    parser.yy.subQueries = [];
+    parser.yy.selectListAliases = [];
+    parser.yy.latestTablePrimaries = [];
+
+    prioritizeSuggestions();
+  };
+
+  parser.yy.parseError = function(message, error) {
+    parser.yy.errors.push(error);
+    return message;
+  };
+
+  parser.addCommonTableExpressions = function(identifiers) {
+    parser.yy.result.commonTableExpressions = identifiers;
+    parser.yy.latestCommonTableExpressions = identifiers;
+  };
+
+  parser.isInSubquery = function() {
+    return !!parser.yy.primariesStack.length;
+  };
+
+  parser.pushQueryState = function() {
+    parser.yy.resultStack.push(parser.yy.result);
+    parser.yy.locationsStack.push(parser.yy.locations);
+    parser.yy.selectListAliasesStack.push(parser.yy.selectListAliases);
+    parser.yy.primariesStack.push(parser.yy.latestTablePrimaries);
+    parser.yy.subQueriesStack.push(parser.yy.subQueries);
+
+    parser.yy.result = {};
+    parser.yy.locations = [];
+    parser.yy.selectListAliases = []; // Not allowed in correlated sub-queries
+
+    if (parser.yy.correlatedSubQuery) {
+      parser.yy.latestTablePrimaries = parser.yy.latestTablePrimaries.concat();
+      parser.yy.subQueries = parser.yy.subQueries.concat();
+    } else {
+      parser.yy.latestTablePrimaries = [];
+      parser.yy.subQueries = [];
+    }
+  };
+
+  parser.popQueryState = function(subQuery) {
+    linkTablePrimaries();
+    parser.commitLocations();
+
+    if (Object.keys(parser.yy.result).length === 0) {
+      parser.yy.result = parser.yy.resultStack.pop();
+    } else {
+      parser.yy.resultStack.pop();
+    }
+    const oldSubQueries = parser.yy.subQueries;
+    parser.yy.subQueries = parser.yy.subQueriesStack.pop();
+    if (subQuery) {
+      if (oldSubQueries.length > 0) {
+        subQuery.subQueries = oldSubQueries;
+      }
+      parser.yy.subQueries.push(subQuery);
+    }
+
+    parser.yy.latestTablePrimaries = parser.yy.primariesStack.pop();
+    parser.yy.locations = parser.yy.locationsStack.pop();
+    parser.yy.selectListAliases = parser.yy.selectListAliasesStack.pop();
+  };
+
+  parser.suggestSelectListAliases = function() {
+    if (
+      parser.yy.selectListAliases &&
+      parser.yy.selectListAliases.length > 0 &&
+      parser.yy.result.suggestColumns &&
+      (typeof parser.yy.result.suggestColumns.identifierChain === 'undefined' ||
+        parser.yy.result.suggestColumns.identifierChain.length === 0)
+    ) {
+      parser.yy.result.suggestColumnAliases = parser.yy.selectListAliases;
+    }
+  };
+
+  parser.mergeSuggestKeywords = function() {
+    let result = [];
+    Array.prototype.slice.call(arguments).forEach(suggestion => {
+      if (typeof suggestion !== 'undefined' && typeof suggestion.suggestKeywords !== 'undefined') {
+        result = result.concat(suggestion.suggestKeywords);
+      }
+    });
+    if (result.length > 0) {
+      return { suggestKeywords: result };
+    }
+    return {};
+  };
+
+  parser.suggestValueExpressionKeywords = function(valueExpression, extras) {
+    const expressionKeywords = parser.getValueExpressionKeywords(valueExpression, extras);
+    parser.suggestKeywords(expressionKeywords.suggestKeywords);
+    if (expressionKeywords.suggestColRefKeywords) {
+      parser.suggestColRefKeywords(expressionKeywords.suggestColRefKeywords);
+    }
+    if (valueExpression.lastType) {
+      parser.addColRefIfExists(valueExpression.lastType);
+    } else {
+      parser.addColRefIfExists(valueExpression);
+    }
+  };
+
+  parser.getSelectListKeywords = function(excludeAsterisk) {
+    let keywords = [{ value: 'CASE', weight: 450 }, 'FALSE', 'TRUE', 'NULL'];
+    if (!excludeAsterisk) {
+      keywords.push({ value: '*', weight: 10000 });
+    }
+    return keywords;
+  };
+
+  parser.getValueExpressionKeywords = function(valueExpression, extras) {
+    const types = valueExpression.lastType ? valueExpression.lastType.types : valueExpression.types;
+    // We could have valueExpression.columnReference to suggest based on column type
+    let keywords = [
+      '<',
+      '<=',
+      '<=>',
+      '<>',
+      '=',
+      '>',
+      '>=',
+      'BETWEEN',
+      'IN',
+      'IS NOT NULL',
+      'IS NULL',
+      'IS NOT TRUE',
+      'IS TRUE',
+      'IS NOT FALSE',
+      'IS FALSE',
+      'NOT BETWEEN',
+      'NOT IN'
+    ];
+    if (extras) {
+      keywords = keywords.concat(extras);
+    }
+    if (valueExpression.suggestKeywords) {
+      keywords = keywords.concat(valueExpression.suggestKeywords);
+    }
+    if (types.length === 1 && types[0] === 'COLREF') {
+      return {
+        suggestKeywords: keywords,
+        suggestColRefKeywords: {
+          BOOLEAN: ['AND', 'OR'],
+          NUMBER: ['+', '-', '*', '/', '%', 'DIV'],
+          STRING: ['LIKE', 'NOT LIKE', 'REGEXP', 'RLIKE']
+        }
+      };
+    }
+    if (
+      typeof SqlFunctions === 'undefined' ||
+      SqlFunctions.matchesType(parser.yy.activeDialect, ['BOOLEAN'], types)
+    ) {
+      keywords = keywords.concat(['AND', 'OR']);
+    }
+    if (
+      typeof SqlFunctions === 'undefined' ||
+      SqlFunctions.matchesType(parser.yy.activeDialect, ['NUMBER'], types)
+    ) {
+      keywords = keywords.concat(['+', '-', '*', '/', '%', 'DIV']);
+    }
+    if (
+      typeof SqlFunctions === 'undefined' ||
+      SqlFunctions.matchesType(parser.yy.activeDialect, ['STRING'], types)
+    ) {
+      keywords = keywords.concat(['LIKE', 'NOT LIKE', 'REGEXP', 'RLIKE']);
+    }
+    return { suggestKeywords: keywords };
+  };
+
+  parser.getTypeKeywords = function() {
+    return [
+      'BIGINT',
+      'BOOLEAN',
+      'CHAR',
+      'DECIMAL',
+      'DOUBLE',
+      'FLOAT',
+      'INT',
+      'SMALLINT',
+      'TIMESTAMP',
+      'STRING',
+      'TINYINT',
+      'VARCHAR'
+    ];
+  };
+
+  parser.getColumnDataTypeKeywords = function() {
+    return parser.getTypeKeywords();
+  };
+
+  parser.addColRefIfExists = function(valueExpression) {
+    if (valueExpression.columnReference) {
+      parser.yy.result.colRef = { identifierChain: valueExpression.columnReference };
+    }
+  };
+
+  parser.selectListNoTableSuggest = function(selectListEdit, hasDistinctOrAll) {
+    if (selectListEdit.cursorAtStart) {
+      let keywords = parser.getSelectListKeywords();
+      if (!hasDistinctOrAll) {
+        keywords = keywords.concat([{ value: 'ALL', weight: 2 }, { value: 'DISTINCT', weight: 2 }]);
+      }
+      parser.suggestKeywords(keywords);
+    } else {
+      parser.checkForKeywords(selectListEdit);
+    }
+    if (selectListEdit.suggestFunctions) {
+      parser.suggestFunctions();
+    }
+    if (selectListEdit.suggestColumns) {
+      parser.suggestColumns();
+    }
+    if (
+      selectListEdit.suggestAggregateFunctions &&
+      (!hasDistinctOrAll || hasDistinctOrAll === 'ALL')
+    ) {
+      parser.suggestAggregateFunctions();
+      parser.suggestAnalyticFunctions();
+    }
+  };
+
+  parser.suggestJoinConditions = function(details) {
+    parser.yy.result.suggestJoinConditions = details || {};
+    if (parser.yy.latestTablePrimaries && !parser.yy.result.suggestJoinConditions.tablePrimaries) {
+      parser.yy.result.suggestJoinConditions.tablePrimaries = parser.yy.latestTablePrimaries.concat();
+    }
+  };
+
+  parser.suggestJoins = function(details) {
+    parser.yy.result.suggestJoins = details || {};
+  };
+
+  parser.valueExpressionSuggest = function(oppositeValueExpression, operator) {
+    if (oppositeValueExpression && oppositeValueExpression.columnReference) {
+      parser.suggestValues();
+      parser.yy.result.colRef = { identifierChain: oppositeValueExpression.columnReference };
+    }
+    parser.suggestColumns();
+    parser.suggestFunctions();
+    let keywords = [
+      { value: 'CASE', weight: 450 },
+      { value: 'FALSE', weight: 450 },
+      { value: 'NULL', weight: 450 },
+      { value: 'TRUE', weight: 450 }
+    ];
+    if (
+      typeof oppositeValueExpression === 'undefined' ||
+      typeof operator === 'undefined'
+    ) {
+      keywords = keywords.concat(['EXISTS', 'NOT']);
+    }
+    if (oppositeValueExpression && oppositeValueExpression.types[0] === 'NUMBER') {
+      parser.applyTypeToSuggestions(['NUMBER']);
+    }
+    parser.suggestKeywords(keywords);
+  };
+
+  parser.applyTypeToSuggestions = function(types) {
+    if (types[0] === 'BOOLEAN') {
+      return;
+    }
+    if (parser.yy.result.suggestFunctions && !parser.yy.result.suggestFunctions.types) {
+      parser.yy.result.suggestFunctions.types = types;
+    }
+    if (parser.yy.result.suggestColumns && !parser.yy.result.suggestColumns.types) {
+      parser.yy.result.suggestColumns.types = types;
+    }
+  };
+
+  parser.findCaseType = function(whenThenList) {
+    const types = {};
+    whenThenList.caseTypes.forEach(valueExpression => {
+      valueExpression.types.forEach(type => {
+        types[type] = true;
+      });
+    });
+    if (Object.keys(types).length === 1) {
+      return { types: [Object.keys(types)[0]] };
+    }
+    return { types: ['T'] };
+  };
+
+  parser.findReturnTypes = function(functionName) {
+    return typeof SqlFunctions === 'undefined'
+      ? ['T']
+      : SqlFunctions.getReturnTypes(parser.yy.activeDialect, functionName.toLowerCase());
+  };
+
+  parser.applyArgumentTypesToSuggestions = function(functionName, position) {
+    const foundArguments =
+      typeof SqlFunctions === 'undefined'
+        ? ['T']
+        : SqlFunctions.getArgumentTypes(
+            parser.yy.activeDialect,
+            functionName.toLowerCase(),
+            position
+          );
+    if (foundArguments.length === 0 && parser.yy.result.suggestColumns) {
+      delete parser.yy.result.suggestColumns;
+      delete parser.yy.result.suggestKeyValues;
+      delete parser.yy.result.suggestValues;
+      delete parser.yy.result.suggestFunctions;
+      delete parser.yy.result.suggestIdentifiers;
+      delete parser.yy.result.suggestKeywords;
+    } else {
+      parser.applyTypeToSuggestions(foundArguments);
+    }
+  };
+
+  parser.commitLocations = function() {
+    if (parser.yy.locations.length === 0) {
+      return;
+    }
+
+    let tablePrimaries = parser.yy.latestTablePrimaries;
+
+    let i = parser.yy.locations.length;
+
+    while (i--) {
+      const location = parser.yy.locations[i];
+      if (location.type === 'variable' && location.colRef) {
+        parser.expandIdentifierChain({
+          wrapper: location.colRef,
+          tablePrimaries: tablePrimaries,
+          isColumnWrapper: true
+        });
+        delete location.colRef.linked;
+      }
+
+      if (location.type === 'unknown') {
+        if (
+          typeof location.identifierChain !== 'undefined' &&
+          location.identifierChain.length > 0 &&
+          location.identifierChain.length <= 2 &&
+          tablePrimaries
+        ) {
+          let found = tablePrimaries.filter(primary => {
+            return (
+              equalIgnoreCase(primary.alias, location.identifierChain[0].name) ||
+              (primary.identifierChain &&
+                equalIgnoreCase(primary.identifierChain[0].name, location.identifierChain[0].name))
+            );
+          });
+          if (!found.length && location.firstInChain) {
+            found = tablePrimaries.filter(primary => {
+              return (
+                !primary.alias &&
+                primary.identifierChain &&
+                equalIgnoreCase(
+                  primary.identifierChain[primary.identifierChain.length - 1].name,
+                  location.identifierChain[0].name
+                )
+              );
+            });
+          }
+
+          if (found.length) {
+            if (
+              found[0].identifierChain.length > 1 &&
+              location.identifierChain.length === 1 &&
+              equalIgnoreCase(found[0].identifierChain[0].name, location.identifierChain[0].name)
+            ) {
+              location.type = 'database';
+            } else if (
+              found[0].alias &&
+              equalIgnoreCase(location.identifierChain[0].name, found[0].alias) &&
+              location.identifierChain.length > 1
+            ) {
+              location.type = 'column';
+              parser.expandIdentifierChain({
+                tablePrimaries: tablePrimaries,
+                wrapper: location,
+                anyOwner: true
+              });
+            } else if (
+              !found[0].alias &&
+              found[0].identifierChain &&
+              equalIgnoreCase(
+                location.identifierChain[0].name,
+                found[0].identifierChain[found[0].identifierChain.length - 1].name
+              ) &&
+              location.identifierChain.length > 1
+            ) {
+              location.type = 'column';
+              parser.expandIdentifierChain({
+                tablePrimaries: tablePrimaries,
+                wrapper: location,
+                anyOwner: true
+              });
+            } else {
+              location.type = 'table';
+              parser.expandIdentifierChain({
+                tablePrimaries: tablePrimaries,
+                wrapper: location,
+                anyOwner: true
+              });
+            }
+          } else if (parser.yy.subQueries) {
+            found = parser.yy.subQueries.filter(subQuery => {
+              return equalIgnoreCase(subQuery.alias, location.identifierChain[0].name);
+            });
+            if (found.length > 0) {
+              location.type = 'subQuery';
+              location.identifierChain = [{ subQuery: found[0].alias }];
+            }
+          }
+        }
+      }
+
+      if (location.type === 'asterisk' && !location.linked) {
+        if (tablePrimaries && tablePrimaries.length > 0) {
+          location.tables = [];
+          location.linked = false;
+          if (!location.identifierChain) {
+            location.identifierChain = [{ asterisk: true }];
+          }
+          parser.expandIdentifierChain({
+            tablePrimaries: tablePrimaries,
+            wrapper: location,
+            anyOwner: false
+          });
+          if (location.tables.length === 0) {
+            parser.yy.locations.splice(i, 1);
+          }
+        } else {
+          parser.yy.locations.splice(i, 1);
+        }
+      }
+
+      if (
+        location.type === 'table' &&
+        typeof location.identifierChain !== 'undefined' &&
+        location.identifierChain.length === 1 &&
+        location.identifierChain[0].name
+      ) {
+        // Could be a cte reference
+        parser.yy.locations.some(otherLocation => {
+          if (
+            otherLocation.type === 'alias' &&
+            otherLocation.source === 'cte' &&
+            identifierEquals(otherLocation.alias, location.identifierChain[0].name)
+          ) {
+            // TODO: Possibly add the other location if we want to show the link in the future.
+            //       i.e. highlight select definition on hover over alias, also for subquery references.
+            location.type = 'alias';
+            location.target = 'cte';
+            location.alias = location.identifierChain[0].name;
+            delete location.identifierChain;
+            return true;
+          }
+        });
+      }
+
+      if (
+        location.type === 'table' &&
+        (typeof location.identifierChain === 'undefined' || location.identifierChain.length === 0)
+      ) {
+        parser.yy.locations.splice(i, 1);
+      }
+
+      if (location.type === 'unknown') {
+        location.type = 'column';
+      }
+
+      // A column location might refer to a previously defined alias, i.e. last 'foo' in "SELECT cast(id AS int) foo FROM tbl ORDER BY foo;"
+      if (location.type === 'column') {
+        for (let j = i - 1; j >= 0; j--) {
+          const otherLocation = parser.yy.locations[j];
+          if (
+            otherLocation.type === 'alias' &&
+            otherLocation.source === 'column' &&
+            location.identifierChain &&
+            location.identifierChain.length === 1 &&
+            location.identifierChain[0].name &&
+            otherLocation.alias &&
+            location.identifierChain[0].name.toLowerCase() === otherLocation.alias.toLowerCase()
+          ) {
+            location.type = 'alias';
+            location.source = 'column';
+            location.alias = location.identifierChain[0].name;
+            delete location.identifierChain;
+            location.parentLocation = otherLocation.parentLocation;
+            break;
+          }
+        }
+      }
+
+      if (location.type === 'column') {
+        const initialIdentifierChain = location.identifierChain
+          ? location.identifierChain.concat()
+          : undefined;
+
+        parser.expandIdentifierChain({
+          tablePrimaries: tablePrimaries,
+          wrapper: location,
+          anyOwner: true,
+          isColumnWrapper: true,
+          isColumnLocation: true
+        });
+
+        if (typeof location.identifierChain === 'undefined') {
+          parser.yy.locations.splice(i, 1);
+        } else if (
+          location.identifierChain.length === 0 &&
+          initialIdentifierChain &&
+          initialIdentifierChain.length === 1
+        ) {
+          // This is for the case "SELECT tblOrColName FROM db.tblOrColName";
+          location.identifierChain = initialIdentifierChain;
+        }
+      }
+      if (location.type === 'column' && location.identifierChain) {
+        if (location.identifierChain.length > 1 && location.tables && location.tables.length > 0) {
+          location.type = 'complex';
+        }
+      }
+      delete location.firstInChain;
+      if (location.type !== 'column' && location.type !== 'complex') {
+        delete location.qualified;
+      } else if (typeof location.qualified === 'undefined') {
+        location.qualified = false;
+      }
+    }
+
+    if (parser.yy.locations.length > 0) {
+      parser.yy.allLocations = parser.yy.allLocations.concat(parser.yy.locations);
+      parser.yy.locations = [];
+    }
+  };
+
+  const prioritizeSuggestions = function() {
+    parser.yy.result.lowerCase = parser.yy.lowerCase || false;
+
+    const cteIndex = {};
+
+    if (typeof parser.yy.latestCommonTableExpressions !== 'undefined') {
+      parser.yy.latestCommonTableExpressions.forEach(cte => {
+        cteIndex[cte.alias.toLowerCase()] = cte;
+      });
+    }
+
+    SIMPLE_TABLE_REF_SUGGESTIONS.forEach(suggestionType => {
+      if (
+        suggestionType !== 'suggestAggregateFunctions' &&
+        typeof parser.yy.result[suggestionType] !== 'undefined' &&
+        parser.yy.result[suggestionType].tables.length === 0
+      ) {
+        delete parser.yy.result[suggestionType];
+      } else if (
+        typeof parser.yy.result[suggestionType] !== 'undefined' &&
+        typeof parser.yy.result[suggestionType].tables !== 'undefined'
+      ) {
+        for (let i = parser.yy.result[suggestionType].tables.length - 1; i >= 0; i--) {
+          const table = parser.yy.result[suggestionType].tables[i];
+          if (
+            table.identifierChain.length === 1 &&
+            typeof table.identifierChain[0].name !== 'undefined' &&
+            typeof cteIndex[table.identifierChain[0].name.toLowerCase()] !== 'undefined'
+          ) {
+            parser.yy.result[suggestionType].tables.splice(i, 1);
+          }
+        }
+      }
+    });
+
+    if (typeof parser.yy.result.colRef !== 'undefined') {
+      if (
+        !parser.yy.result.colRef.linked ||
+        typeof parser.yy.result.colRef.identifierChain === 'undefined' ||
+        parser.yy.result.colRef.identifierChain.length === 0
+      ) {
+        delete parser.yy.result.colRef;
+        if (typeof parser.yy.result.suggestColRefKeywords !== 'undefined') {
+          Object.keys(parser.yy.result.suggestColRefKeywords).forEach(type => {
+            parser.yy.result.suggestKeywords = parser.yy.result.suggestKeywords.concat(
+              parser.createWeightedKeywords(parser.yy.result.suggestColRefKeywords[type], -1)
+            );
+          });
+          delete parser.yy.result.suggestColRefKeywords;
+        }
+        if (
+          parser.yy.result.suggestColumns &&
+          parser.yy.result.suggestColumns.types.length === 1 &&
+          parser.yy.result.suggestColumns.types[0] === 'COLREF'
+        ) {
+          parser.yy.result.suggestColumns.types = ['T'];
+        }
+        delete parser.yy.result.suggestValues;
+      }
+    }
+
+    if (typeof parser.yy.result.colRef !== 'undefined') {
+      if (
+        !parser.yy.result.suggestValues &&
+        !parser.yy.result.suggestColRefKeywords &&
+        (!parser.yy.result.suggestColumns || parser.yy.result.suggestColumns.types[0] !== 'COLREF')
+      ) {
+        delete parser.yy.result.colRef;
+      }
+    }
+    if (
+      typeof parser.yy.result.suggestIdentifiers !== 'undefined' &&
+      parser.yy.result.suggestIdentifiers.length > 0
+    ) {
+      delete parser.yy.result.suggestTables;
+      delete parser.yy.result.suggestDatabases;
+    }
+    if (typeof parser.yy.result.suggestColumns !== 'undefined') {
+      const suggestColumns = parser.yy.result.suggestColumns;
+      if (typeof suggestColumns.tables === 'undefined' || suggestColumns.tables.length === 0) {
+        delete parser.yy.result.suggestColumns;
+        delete parser.yy.result.subQueries;
+      } else {
+        delete parser.yy.result.suggestTables;
+        delete parser.yy.result.suggestDatabases;
+
+        suggestColumns.tables.forEach(table => {
+          if (
+            typeof table.identifierChain !== 'undefined' &&
+            table.identifierChain.length === 1 &&
+            typeof table.identifierChain[0].name !== 'undefined'
+          ) {
+            const cte = cteIndex[table.identifierChain[0].name.toLowerCase()];
+            if (typeof cte !== 'undefined') {
+              delete table.identifierChain[0].name;
+              table.identifierChain[0].cte = cte.alias;
+            }
+          } else if (typeof table.identifierChain === 'undefined' && table.subQuery) {
+            table.identifierChain = [{ subQuery: table.subQuery }];
+            delete table.subQuery;
+          }
+        });
+
+        if (
+          typeof suggestColumns.identifierChain !== 'undefined' &&
+          suggestColumns.identifierChain.length === 0
+        ) {
+          delete suggestColumns.identifierChain;
+        }
+      }
+    } else {
+      delete parser.yy.result.subQueries;
+    }
+
+    if (typeof parser.yy.result.suggestJoinConditions !== 'undefined') {
+      if (
+        typeof parser.yy.result.suggestJoinConditions.tables === 'undefined' ||
+        parser.yy.result.suggestJoinConditions.tables.length === 0
+      ) {
+        delete parser.yy.result.suggestJoinConditions;
+      }
+    }
+
+    if (
+      typeof parser.yy.result.suggestTables !== 'undefined' &&
+      typeof parser.yy.result.commonTableExpressions !== 'undefined'
+    ) {
+      const ctes = [];
+      parser.yy.result.commonTableExpressions.forEach(cte => {
+        const suggestion = { name: cte.alias };
+        if (parser.yy.result.suggestTables.prependFrom) {
+          suggestion.prependFrom = true;
+        }
+        if (parser.yy.result.suggestTables.prependQuestionMark) {
+          suggestion.prependQuestionMark = true;
+        }
+        ctes.push(suggestion);
+      });
+      if (ctes.length > 0) {
+        parser.yy.result.suggestCommonTableExpressions = ctes;
+      }
+    }
+  };
+
+  parser.identifyPartials = function(beforeCursor, afterCursor) {
+    const beforeMatch = beforeCursor.match(/[0-9a-zA-Z_]*$/);
+    const afterMatch = afterCursor.match(/^[0-9a-zA-Z_]*(?:\((?:[^)]*\))?)?/);
+    return {
+      left: beforeMatch ? beforeMatch[0].length : 0,
+      right: afterMatch ? afterMatch[0].length : 0
+    };
+  };
+
+  const addCleanTablePrimary = function(tables, tablePrimary) {
+    if (tablePrimary.alias) {
+      tables.push({ alias: tablePrimary.alias, identifierChain: tablePrimary.identifierChain });
+    } else {
+      tables.push({ identifierChain: tablePrimary.identifierChain });
+    }
+  };
+
+  parser.expandIdentifierChain = function(options) {
+    const wrapper = options.wrapper;
+    const anyOwner = options.anyOwner;
+    const isColumnWrapper = options.isColumnWrapper;
+    const isColumnLocation = options.isColumnLocation;
+    let tablePrimaries = options.tablePrimaries || parser.yy.latestTablePrimaries;
+
+    if (typeof wrapper.identifierChain === 'undefined' || typeof tablePrimaries === 'undefined') {
+      return;
+    }
+    let identifierChain = wrapper.identifierChain.concat();
+
+    if (tablePrimaries.length === 0) {
+      delete wrapper.identifierChain;
+      return;
+    }
+
+    if (!anyOwner) {
+      tablePrimaries = filterTablePrimariesForOwner(tablePrimaries, wrapper.owner);
+    }
+
+    if (identifierChain.length > 0 && identifierChain[identifierChain.length - 1].asterisk) {
+      const tables = [];
+      tablePrimaries.forEach(tablePrimary => {
+        if (identifierChain.length > 1 && !tablePrimary.subQueryAlias) {
+          if (
+            identifierChain.length === 2 &&
+            equalIgnoreCase(tablePrimary.alias, identifierChain[0].name)
+          ) {
+            addCleanTablePrimary(tables, tablePrimary);
+          } else if (
+            identifierChain.length === 2 &&
+            equalIgnoreCase(tablePrimary.identifierChain[0].name, identifierChain[0].name)
+          ) {
+            addCleanTablePrimary(tables, tablePrimary);
+          } else if (
+            identifierChain.length === 3 &&
+            tablePrimary.identifierChain.length > 1 &&
+            equalIgnoreCase(tablePrimary.identifierChain[0].name, identifierChain[0].name) &&
+            equalIgnoreCase(tablePrimary.identifierChain[1].name, identifierChain[1].name)
+          ) {
+            addCleanTablePrimary(tables, tablePrimary);
+          }
+        } else if (tablePrimary.subQueryAlias) {
+          tables.push({ identifierChain: [{ subQuery: tablePrimary.subQueryAlias }] });
+        } else {
+          addCleanTablePrimary(tables, tablePrimary);
+        }
+      });
+      // Possible Joins
+      if (tables.length > 0) {
+        wrapper.tables = tables;
+        delete wrapper.identifierChain;
+        return;
+      }
+    }
+
+    // IdentifierChain contains a possibly started identifier or empty, example: a.b.c = ['a', 'b', 'c']
+    // Reduce the tablePrimaries to the one that matches the first identifier if found
+    let foundPrimary;
+    let doubleMatch = false;
+    let aliasMatch = false;
+    if (identifierChain.length > 0) {
+      for (let i = 0; i < tablePrimaries.length; i++) {
+        if (tablePrimaries[i].subQueryAlias) {
+          if (equalIgnoreCase(tablePrimaries[i].subQueryAlias, identifierChain[0].name)) {
+            foundPrimary = tablePrimaries[i];
+          }
+        } else if (equalIgnoreCase(tablePrimaries[i].alias, identifierChain[0].name)) {
+          foundPrimary = tablePrimaries[i];
+          aliasMatch = true;
+          break;
+        } else if (
+          tablePrimaries[i].identifierChain.length > 1 &&
+          identifierChain.length > 1 &&
+          equalIgnoreCase(tablePrimaries[i].identifierChain[0].name, identifierChain[0].name) &&
+          equalIgnoreCase(tablePrimaries[i].identifierChain[1].name, identifierChain[1].name)
+        ) {
+          foundPrimary = tablePrimaries[i];
+          doubleMatch = true;
+          break;
+        } else if (
+          !foundPrimary &&
+          equalIgnoreCase(tablePrimaries[i].identifierChain[0].name, identifierChain[0].name) &&
+          identifierChain.length > (isColumnLocation ? 1 : 0)
+        ) {
+          foundPrimary = tablePrimaries[i];
+          // No break as first two can still match.
+        } else if (
+          !foundPrimary &&
+          tablePrimaries[i].identifierChain.length > 1 &&
+          !tablePrimaries[i].alias &&
+          equalIgnoreCase(
+            tablePrimaries[i].identifierChain[tablePrimaries[i].identifierChain.length - 1].name,
+            identifierChain[0].name
+          )
+        ) {
+          // This is for the case SELECT baa. FROM bla.baa, blo.boo;
+          foundPrimary = tablePrimaries[i];
+          break;
+        }
+      }
+    }
+
+    if (foundPrimary) {
+      identifierChain.shift();
+      if (doubleMatch) {
+        identifierChain.shift();
+      }
+    } else if (tablePrimaries.length === 1 && !isColumnWrapper) {
+      foundPrimary = tablePrimaries[0];
+    }
+
+    if (foundPrimary) {
+      if (isColumnWrapper) {
+        wrapper.identifierChain = identifierChain;
+        if (foundPrimary.subQueryAlias) {
+          wrapper.tables = [{ subQuery: foundPrimary.subQueryAlias }];
+        } else if (foundPrimary.alias) {
+          if (!isColumnLocation && isColumnWrapper && aliasMatch) {
+            // TODO: add alias on table in suggestColumns (needs support in sqlAutocomplete3.js)
+            // the case is: SELECT cu.| FROM customers cu;
+            // This prevents alias from being added automatically in sqlAutocompleter.js
+            wrapper.tables = [{ identifierChain: foundPrimary.identifierChain }];
+          } else {
+            wrapper.tables = [
+              { identifierChain: foundPrimary.identifierChain, alias: foundPrimary.alias }
+            ];
+          }
+        } else {
+          wrapper.tables = [{ identifierChain: foundPrimary.identifierChain }];
+        }
+      } else {
+        if (foundPrimary.subQueryAlias) {
+          identifierChain.unshift({ subQuery: foundPrimary.subQueryAlias });
+        } else {
+          identifierChain = foundPrimary.identifierChain.concat(identifierChain);
+        }
+        if (wrapper.tables) {
+          wrapper.tables.push({ identifierChain: identifierChain });
+          delete wrapper.identifierChain;
+        } else {
+          wrapper.identifierChain = identifierChain;
+        }
+      }
+    } else {
+      if (isColumnWrapper) {
+        wrapper.tables = [];
+      }
+      tablePrimaries.forEach(tablePrimary => {
+        const targetTable = tablePrimary.subQueryAlias
+          ? { subQuery: tablePrimary.subQueryAlias }
+          : { identifierChain: tablePrimary.identifierChain };
+        if (tablePrimary.alias) {
+          targetTable.alias = tablePrimary.alias;
+        }
+        if (wrapper.tables) {
+          wrapper.tables.push(targetTable);
+        }
+      });
+    }
+    delete wrapper.owner;
+    wrapper.linked = true;
+  };
+
+  const filterTablePrimariesForOwner = function(tablePrimaries, owner) {
+    const result = [];
+    tablePrimaries.forEach(primary => {
+      if (typeof owner === 'undefined' && typeof primary.owner === 'undefined') {
+        result.push(primary);
+      } else if (owner === primary.owner) {
+        result.push(primary);
+      }
+    });
+    return result;
+  };
+
+  const convertTablePrimariesToSuggestions = function(tablePrimaries) {
+    const tables = [];
+    const identifiers = [];
+    tablePrimaries.forEach(tablePrimary => {
+      if (tablePrimary.identifierChain && tablePrimary.identifierChain.length > 0) {
+        const table = { identifierChain: tablePrimary.identifierChain };
+        if (tablePrimary.alias) {
+          table.alias = tablePrimary.alias;
+          identifiers.push({ name: table.alias + '.', type: 'alias' });
+        } else {
+          const lastIdentifier =
+            tablePrimary.identifierChain[tablePrimary.identifierChain.length - 1];
+          if (typeof lastIdentifier.name !== 'undefined') {
+            identifiers.push({ name: lastIdentifier.name + '.', type: 'table' });
+          } else if (typeof lastIdentifier.subQuery !== 'undefined') {
+            identifiers.push({ name: lastIdentifier.subQuery + '.', type: 'sub-query' });
+          }
+        }
+        tables.push(table);
+      } else if (tablePrimary.subQueryAlias) {
+        identifiers.push({ name: tablePrimary.subQueryAlias + '.', type: 'sub-query' });
+        tables.push({ identifierChain: [{ subQuery: tablePrimary.subQueryAlias }] });
+      }
+    });
+    if (identifiers.length > 0) {
+      if (typeof parser.yy.result.suggestIdentifiers === 'undefined') {
+        parser.yy.result.suggestIdentifiers = identifiers;
+      } else {
+        parser.yy.result.suggestIdentifiers = identifiers.concat(
+          parser.yy.result.suggestIdentifiers
+        );
+      }
+    }
+    parser.yy.result.suggestColumns.tables = tables;
+    if (
+      parser.yy.result.suggestColumns.identifierChain &&
+      parser.yy.result.suggestColumns.identifierChain.length === 0
+    ) {
+      delete parser.yy.result.suggestColumns.identifierChain;
+    }
+    parser.yy.result.suggestColumns.linked = true;
+  };
+
+  const linkTablePrimaries = function() {
+    if (!parser.yy.cursorFound || typeof parser.yy.latestTablePrimaries === 'undefined') {
+      return;
+    }
+
+    SIMPLE_TABLE_REF_SUGGESTIONS.forEach(suggestionType => {
+      if (
+        typeof parser.yy.result[suggestionType] !== 'undefined' &&
+        parser.yy.result[suggestionType].tablePrimaries &&
+        !parser.yy.result[suggestionType].linked
+      ) {
+        parser.yy.result[suggestionType].tables = [];
+        parser.yy.result[suggestionType].tablePrimaries.forEach(tablePrimary => {
+          if (!tablePrimary.subQueryAlias) {
+            parser.yy.result[suggestionType].tables.push(
+              tablePrimary.alias
+                ? {
+                    identifierChain: tablePrimary.identifierChain.concat(),
+                    alias: tablePrimary.alias
+                  }
+                : { identifierChain: tablePrimary.identifierChain.concat() }
+            );
+          }
+        });
+        delete parser.yy.result[suggestionType].tablePrimaries;
+        parser.yy.result[suggestionType].linked = true;
+      }
+    });
+
+    if (
+      typeof parser.yy.result.suggestColumns !== 'undefined' &&
+      !parser.yy.result.suggestColumns.linked
+    ) {
+      const tablePrimaries = filterTablePrimariesForOwner(
+        parser.yy.latestTablePrimaries,
+        parser.yy.result.suggestColumns.owner
+      );
+      if (!parser.yy.result.suggestColumns.tables) {
+        parser.yy.result.suggestColumns.tables = [];
+      }
+      if (parser.yy.subQueries.length > 0) {
+        parser.yy.result.subQueries = parser.yy.subQueries;
+      }
+      if (
+        typeof parser.yy.result.suggestColumns.identifierChain === 'undefined' ||
+        parser.yy.result.suggestColumns.identifierChain.length === 0
+      ) {
+        if (tablePrimaries.length > 1) {
+          convertTablePrimariesToSuggestions(tablePrimaries);
+        } else {
+          if (
+            tablePrimaries.length === 1 &&
+            (tablePrimaries[0].alias || tablePrimaries[0].subQueryAlias)
+          ) {
+            convertTablePrimariesToSuggestions(tablePrimaries);
+          }
+          parser.expandIdentifierChain({
+            wrapper: parser.yy.result.suggestColumns,
+            anyOwner: false,
+            isColumnWrapper: true
+          });
+        }
+      } else {
+        parser.expandIdentifierChain({
+          wrapper: parser.yy.result.suggestColumns,
+          anyOwner: false,
+          isColumnWrapper: true
+        });
+      }
+    }
+
+    if (typeof parser.yy.result.colRef !== 'undefined' && !parser.yy.result.colRef.linked) {
+      parser.expandIdentifierChain({ wrapper: parser.yy.result.colRef });
+
+      const primaries = filterTablePrimariesForOwner(parser.yy.latestTablePrimaries);
+      if (
+        primaries.length === 0 ||
+        (primaries.length > 1 && parser.yy.result.colRef.identifierChain.length === 1)
+      ) {
+        parser.yy.result.colRef.identifierChain = [];
+      }
+    }
+    if (
+      typeof parser.yy.result.suggestKeyValues !== 'undefined' &&
+      !parser.yy.result.suggestKeyValues.linked
+    ) {
+      parser.expandIdentifierChain({ wrapper: parser.yy.result.suggestKeyValues });
+    }
+  };
+
+  parser.getSubQuery = function(cols) {
+    const columns = [];
+    cols.selectList.forEach(col => {
+      const result = {};
+      if (col.alias) {
+        result.alias = col.alias;
+      }
+      if (col.valueExpression && col.valueExpression.columnReference) {
+        result.identifierChain = col.valueExpression.columnReference;
+      } else if (col.asterisk) {
+        result.identifierChain = [{ asterisk: true }];
+      }
+      if (
+        col.valueExpression &&
+        col.valueExpression.types &&
+        col.valueExpression.types.length === 1
+      ) {
+        result.type = col.valueExpression.types[0];
+      }
+
+      columns.push(result);
+    });
+
+    return {
+      columns: columns
+    };
+  };
+
+  parser.addTablePrimary = function(ref) {
+    if (typeof parser.yy.latestTablePrimaries === 'undefined') {
+      parser.yy.latestTablePrimaries = [];
+    }
+    parser.yy.latestTablePrimaries.push(ref);
+  };
+
+  parser.suggestFileFormats = function() {
+    parser.suggestKeywords([
+      'AVRO',
+      'KUDU',
+      'ORC',
+      'PARQUET',
+      'RCFILE',
+      'SEQUENCEFILE',
+      'TEXTFILE'
+    ]);
+  };
+
+  parser.getKeywordsForOptionalsLR = function(optionals, keywords, override) {
+    let result = [];
+
+    for (let i = 0; i < optionals.length; i++) {
+      if (!optionals[i] && (typeof override === 'undefined' || override[i])) {
+        if (keywords[i] instanceof Array) {
+          result = result.concat(keywords[i]);
+        } else {
+          result.push(keywords[i]);
+        }
+      } else if (optionals[i]) {
+        break;
+      }
+    }
+    return result;
+  };
+
+  parser.suggestDdlAndDmlKeywords = function(extraKeywords) {
+    let keywords = [
+      'ALTER',
+      'CREATE',
+      'DESCRIBE',
+      'DROP',
+      'GRANT',
+      'INSERT',
+      'REVOKE',
+      'SELECT',
+      'SET',
+      'SHOW',
+      'TRUNCATE',
+      'UPDATE',
+      'USE',
+      'WITH'
+    ];
+
+    if (extraKeywords) {
+      keywords = keywords.concat(extraKeywords);
+    }
+
+    parser.suggestKeywords(keywords);
+  };
+
+  parser.checkForSelectListKeywords = function(selectList) {
+    if (selectList.length === 0) {
+      return;
+    }
+    const last = selectList[selectList.length - 1];
+    if (!last || !last.valueExpression) {
+      return;
+    }
+    const valueExpressionKeywords = parser.getValueExpressionKeywords(last.valueExpression);
+    let keywords = [];
+    if (last.suggestKeywords) {
+      keywords = keywords.concat(last.suggestKeywords);
+    }
+    if (valueExpressionKeywords.suggestKeywords) {
+      keywords = keywords.concat(valueExpressionKeywords.suggestKeywords);
+    }
+    if (valueExpressionKeywords.suggestColRefKeywords) {
+      parser.suggestColRefKeywords(valueExpressionKeywords.suggestColRefKeywords);
+      parser.addColRefIfExists(last.valueExpression);
+    }
+    if (!last.alias) {
+      keywords.push('AS');
+    }
+    if (keywords.length > 0) {
+      parser.suggestKeywords(keywords);
+    }
+  };
+
+  parser.checkForKeywords = function(expression) {
+    if (expression) {
+      if (expression.suggestKeywords && expression.suggestKeywords.length > 0) {
+        parser.suggestKeywords(expression.suggestKeywords);
+      }
+      if (expression.suggestColRefKeywords) {
+        parser.suggestColRefKeywords(expression.suggestColRefKeywords);
+        parser.addColRefIfExists(expression);
+      }
+    }
+  };
+
+  parser.createWeightedKeywords = function(keywords, weight) {
+    const result = [];
+    keywords.forEach(keyword => {
+      if (typeof keyword.weight !== 'undefined') {
+        keyword.weight = weight + keyword.weight / 10;
+        result.push(keyword);
+      } else {
+        result.push({ value: keyword, weight: weight });
+      }
+    });
+    return result;
+  };
+
+  parser.suggestKeywords = function(keywords) {
+    const weightedKeywords = [];
+    if (keywords.length === 0) {
+      return;
+    }
+    keywords.forEach(keyword => {
+      if (typeof keyword.weight !== 'undefined') {
+        weightedKeywords.push(keyword);
+      } else {
+        weightedKeywords.push({ value: keyword, weight: -1 });
+      }
+    });
+    weightedKeywords.sort((a, b) => {
+      if (a.weight !== b.weight) {
+        return b.weight - a.weight;
+      }
+      return a.value.localeCompare(b.value);
+    });
+    parser.yy.result.suggestKeywords = weightedKeywords;
+  };
+
+  parser.suggestColRefKeywords = function(colRefKeywords) {
+    parser.yy.result.suggestColRefKeywords = colRefKeywords;
+  };
+
+  parser.suggestTablesOrColumns = function(identifier) {
+    if (typeof parser.yy.latestTablePrimaries == 'undefined') {
+      parser.suggestTables({ identifierChain: [{ name: identifier }] });
+      return;
+    }
+    const tableRef = parser.yy.latestTablePrimaries.filter(tablePrimary => {
+      return equalIgnoreCase(tablePrimary.alias, identifier);
+    });
+    if (tableRef.length > 0) {
+      parser.suggestColumns({ identifierChain: [{ name: identifier }] });
+    } else {
+      parser.suggestTables({ identifierChain: [{ name: identifier }] });
+    }
+  };
+
+  parser.suggestFunctions = function(details) {
+    parser.yy.result.suggestFunctions = details || {};
+  };
+
+  parser.suggestAggregateFunctions = function() {
+    const primaries = [];
+    const aliases = {};
+    parser.yy.latestTablePrimaries.forEach(primary => {
+      if (typeof primary.alias !== 'undefined') {
+        aliases[primary.alias] = true;
+      }
+      // Drop if the first one refers to a table alias (...FROM tbl t, t.map tm ...)
+      if (
+        typeof primary.identifierChain !== 'undefined' &&
+        !aliases[primary.identifierChain[0].name] &&
+        typeof primary.owner === 'undefined'
+      ) {
+        primaries.push(primary);
+      }
+    });
+    parser.yy.result.suggestAggregateFunctions = { tablePrimaries: primaries };
+  };
+
+  parser.suggestAnalyticFunctions = function() {
+    parser.yy.result.suggestAnalyticFunctions = true;
+  };
+
+  parser.suggestSetOptions = function() {
+    parser.yy.result.suggestSetOptions = true;
+  };
+
+  parser.suggestIdentifiers = function(identifiers) {
+    parser.yy.result.suggestIdentifiers = identifiers;
+  };
+
+  parser.suggestColumns = function(details) {
+    if (typeof details === 'undefined') {
+      details = { identifierChain: [] };
+    } else if (typeof details.identifierChain === 'undefined') {
+      details.identifierChain = [];
+    }
+    parser.yy.result.suggestColumns = details;
+  };
+
+  parser.suggestGroupBys = function(details) {
+    parser.yy.result.suggestGroupBys = details || {};
+  };
+
+  parser.suggestOrderBys = function(details) {
+    parser.yy.result.suggestOrderBys = details || {};
+  };
+
+  parser.suggestFilters = function(details) {
+    parser.yy.result.suggestFilters = details || {};
+  };
+
+  parser.suggestKeyValues = function(details) {
+    parser.yy.result.suggestKeyValues = details || {};
+  };
+
+  parser.suggestTables = function(details) {
+    parser.yy.result.suggestTables = details || {};
+  };
+
+  const adjustLocationForCursor = function(location) {
+    // columns are 0-based and lines not, so add 1 to cols
+    const newLocation = {
+      first_line: location.first_line,
+      last_line: location.last_line,
+      first_column: location.first_column + 1,
+      last_column: location.last_column + 1
+    };
+    if (parser.yy.cursorFound) {
+      if (
+        parser.yy.cursorFound.first_line === newLocation.first_line &&
+        parser.yy.cursorFound.last_column <= newLocation.first_column
+      ) {
+        let additionalSpace = parser.yy.partialLengths.left + parser.yy.partialLengths.right;
+        additionalSpace -= parser.yy.partialCursor ? 1 : 3; // For some reason the normal cursor eats 3 positions.
+        newLocation.first_column = newLocation.first_column + additionalSpace;
+        newLocation.last_column = newLocation.last_column + additionalSpace;
+      }
+    }
+    return newLocation;
+  };
+
+  parser.addFunctionLocation = function(location, functionName) {
+    // Remove trailing '(' from location
+    const adjustedLocation = {
+      first_line: location.first_line,
+      last_line: location.last_line,
+      first_column: location.first_column,
+      last_column: location.last_column - 1
+    };
+    parser.yy.locations.push({
+      type: 'function',
+      location: adjustLocationForCursor(adjustedLocation),
+      function: functionName.toLowerCase()
+    });
+  };
+
+  parser.addStatementLocation = function(location) {
+    // Don't report lonely cursor as a statement
+    if (
+      location.first_line === location.last_line &&
+      Math.abs(location.last_column - location.first_column) === 1
+    ) {
+      return;
+    }
+    let adjustedLocation;
+    if (
+      parser.yy.cursorFound &&
+      parser.yy.cursorFound.last_line === location.last_line &&
+      parser.yy.cursorFound.first_column >= location.first_column &&
+      parser.yy.cursorFound.last_column <= location.last_column
+    ) {
+      const additionalSpace = parser.yy.partialLengths.left + parser.yy.partialLengths.right;
+      adjustedLocation = {
+        first_line: location.first_line,
+        last_line: location.last_line,
+        first_column: location.first_column + 1,
+        last_column: location.last_column + additionalSpace - (parser.yy.partialCursor ? 0 : 2)
+      };
+    } else {
+      adjustedLocation = {
+        first_line: location.first_line,
+        last_line: location.last_line,
+        first_column: location.first_column + 1,
+        last_column: location.last_column + 1
+      };
+    }
+
+    parser.yy.locations.push({
+      type: 'statement',
+      location: adjustedLocation
+    });
+  };
+
+  parser.firstDefined = function() {
+    for (let i = 0; i + 1 < arguments.length; i += 2) {
+      if (arguments[i]) {
+        return arguments[i + 1];
+      }
+    }
+  };
+
+  parser.addClauseLocation = function(type, precedingLocation, locationIfPresent, isCursor) {
+    let location;
+    if (isCursor) {
+      if (parser.yy.partialLengths.left === 0 && parser.yy.partialLengths.right === 0) {
+        location = {
+          type: type,
+          missing: true,
+          location: adjustLocationForCursor({
+            first_line: precedingLocation.last_line,
+            first_column: precedingLocation.last_column,
+            last_line: precedingLocation.last_line,
+            last_column: precedingLocation.last_column
+          })
+        };
+      } else {
+        location = {
+          type: type,
+          missing: false,
+          location: {
+            first_line: locationIfPresent.last_line,
+            first_column: locationIfPresent.last_column - 1,
+            last_line: locationIfPresent.last_line,
+            last_column:
+              locationIfPresent.last_column -
+              1 +
+              parser.yy.partialLengths.right +
+              parser.yy.partialLengths.left
+          }
+        };
+      }
+    } else {
+      location = {
+        type: type,
+        missing: !locationIfPresent,
+        location: adjustLocationForCursor(
+          locationIfPresent || {
+            first_line: precedingLocation.last_line,
+            first_column: precedingLocation.last_column,
+            last_line: precedingLocation.last_line,
+            last_column: precedingLocation.last_column
+          }
+        )
+      };
+    }
+    if (parser.isInSubquery()) {
+      location.subquery = true;
+    }
+    parser.yy.locations.push(location);
+  };
+
+  parser.addStatementTypeLocation = function(identifier, location, additionalText) {
+    // Don't add if already there except for SELECT
+    if (identifier !== 'SELECT' && parser.yy.allLocations) {
+      for (let i = parser.yy.allLocations.length - 1; i >= 0; i--) {
+        if (parser.yy.allLocations[i] && parser.yy.allLocations[i].type === 'statement') {
+          break;
+        }
+        if (parser.yy.allLocations[i] && parser.yy.allLocations[i].type === 'statementType') {
+          return;
+        }
+      }
+    }
+    const loc = {
+      type: 'statementType',
+      location: adjustLocationForCursor(location),
+      identifier: identifier
+    };
+    if (typeof additionalText !== 'undefined') {
+      switch (identifier) {
+        case 'ALTER':
+          if (/ALTER\s+VIEW/i.test(additionalText)) {
+            loc.identifier = 'ALTER VIEW';
+          } else {
+            loc.identifier = 'ALTER TABLE';
+          }
+          break;
+        case 'COMPUTE':
+          loc.identifier = 'COMPUTE STATS';
+          break;
+        case 'CREATE':
+          if (/CREATE\s+VIEW/i.test(additionalText)) {
+            loc.identifier = 'CREATE VIEW';
+          } else if (/CREATE\s+TABLE/i.test(additionalText)) {
+            loc.identifier = 'CREATE TABLE';
+          } else if (/CREATE\s+DATABASE/i.test(additionalText)) {
+            loc.identifier = 'CREATE DATABASE';
+          } else if (/CREATE\s+ROLE/i.test(additionalText)) {
+            loc.identifier = 'CREATE ROLE';
+          } else if (/CREATE\s+FUNCTION/i.test(additionalText)) {
+            loc.identifier = 'CREATE FUNCTION';
+          } else {
+            loc.identifier = 'CREATE TABLE';
+          }
+          break;
+        case 'DROP':
+          if (/DROP\s+VIEW/i.test(additionalText)) {
+            loc.identifier = 'DROP VIEW';
+          } else if (/DROP\s+TABLE/i.test(additionalText)) {
+            loc.identifier = 'DROP TABLE';
+          } else if (/DROP\s+DATABASE/i.test(additionalText)) {
+            loc.identifier = 'DROP DATABASE';
+          } else if (/DROP\s+ROLE/i.test(additionalText)) {
+            loc.identifier = 'DROP ROLE';
+          } else if (/DROP\s+STATS/i.test(additionalText)) {
+            loc.identifier = 'DROP STATS';
+          } else if (/DROP\s+FUNCTION/i.test(additionalText)) {
+            loc.identifier = 'DROP FUNCTION';
+          } else {
+            loc.identifier = 'DROP TABLE';
+          }
+          break;
+        case 'INVALIDATE':
+          loc.identifier = 'INVALIDATE METADATA';
+          break;
+        case 'LOAD':
+          loc.identifier = 'LOAD DATA';
+          break;
+        case 'TRUNCATE':
+          loc.identifier = 'TRUNCATE TABLE';
+          break;
+        default:
+      }
+    }
+    parser.yy.locations.push(loc);
+  };
+
+  parser.addFileLocation = function(location, path) {
+    parser.yy.locations.push({
+      type: 'file',
+      location: adjustLocationForCursor(location),
+      path: path
+    });
+  };
+
+  parser.addDatabaseLocation = function(location, identifierChain) {
+    parser.yy.locations.push({
+      type: 'database',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addTableLocation = function(location, identifierChain) {
+    parser.yy.locations.push({
+      type: 'table',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addColumnAliasLocation = function(location, alias, parentLocation) {
+    const aliasLocation = {
+      type: 'alias',
+      source: 'column',
+      alias: alias,
+      location: adjustLocationForCursor(location),
+      parentLocation: adjustLocationForCursor(parentLocation)
+    };
+    if (
+      parser.yy.locations.length &&
+      parser.yy.locations[parser.yy.locations.length - 1].type === 'column'
+    ) {
+      const closestColumn = parser.yy.locations[parser.yy.locations.length - 1];
+      if (
+        closestColumn.location.first_line === aliasLocation.parentLocation.first_line &&
+        closestColumn.location.last_line === aliasLocation.parentLocation.last_line &&
+        closestColumn.location.first_column === aliasLocation.parentLocation.first_column &&
+        closestColumn.location.last_column === aliasLocation.parentLocation.last_column
+      ) {
+        parser.yy.locations[parser.yy.locations.length - 1].alias = alias;
+      }
+    }
+    parser.yy.locations.push(aliasLocation);
+  };
+
+  parser.addTableAliasLocation = function(location, alias, identifierChain) {
+    parser.yy.locations.push({
+      type: 'alias',
+      source: 'table',
+      alias: alias,
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addSubqueryAliasLocation = function(location, alias) {
+    parser.yy.locations.push({
+      type: 'alias',
+      source: 'subquery',
+      alias: alias,
+      location: adjustLocationForCursor(location)
+    });
+  };
+
+  parser.addAsteriskLocation = function(location, identifierChain) {
+    parser.yy.locations.push({
+      type: 'asterisk',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addVariableLocation = function(location, value) {
+    if (/\${[^}]*}/.test(value)) {
+      parser.yy.locations.push({
+        type: 'variable',
+        location: adjustLocationForCursor(location),
+        value: value
+      });
+    }
+  };
+
+  parser.addColumnLocation = function(location, identifierChain) {
+    const isVariable =
+      identifierChain.length && /\${[^}]*}/.test(identifierChain[identifierChain.length - 1].name);
+    if (isVariable) {
+      parser.yy.locations.push({
+        type: 'variable',
+        location: adjustLocationForCursor(location),
+        value: identifierChain[identifierChain.length - 1].name
+      });
+    } else {
+      parser.yy.locations.push({
+        type: 'column',
+        location: adjustLocationForCursor(location),
+        identifierChain: identifierChain,
+        qualified: identifierChain.length > 1
+      });
+    }
+  };
+
+  parser.addCteAliasLocation = function(location, alias) {
+    parser.yy.locations.push({
+      type: 'alias',
+      source: 'cte',
+      alias: alias,
+      location: adjustLocationForCursor(location)
+    });
+  };
+
+  parser.addUnknownLocation = function(location, identifierChain) {
+    const isVariable =
+      identifierChain.length && /\${[^}]*}/.test(identifierChain[identifierChain.length - 1].name);
+    let loc;
+    if (isVariable) {
+      loc = {
+        type: 'variable',
+        location: adjustLocationForCursor(location),
+        value: identifierChain[identifierChain.length - 1].name
+      };
+    } else {
+      loc = {
+        type: 'unknown',
+        location: adjustLocationForCursor(location),
+        identifierChain: identifierChain,
+        qualified: identifierChain.length > 1
+      };
+    }
+    parser.yy.locations.push(loc);
+    return loc;
+  };
+
+  parser.addNewDatabaseLocation = function(location, identifierChain) {
+    parser.yy.definitions.push({
+      type: 'database',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addNewTableLocation = function(location, identifierChain, colSpec) {
+    const columns = [];
+    if (colSpec) {
+      colSpec.forEach(col => {
+        columns.push({
+          identifierChain: [col.identifier], // TODO: Complex
+          type: col.type,
+          location: adjustLocationForCursor(col.location)
+        });
+      });
+    }
+    parser.yy.definitions.push({
+      type: 'table',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain,
+      columns: columns
+    });
+  };
+
+  parser.addColRefToVariableIfExists = function(left, right) {
+    if (
+      left &&
+      left.columnReference &&
+      left.columnReference.length &&
+      right &&
+      right.columnReference &&
+      right.columnReference.length &&
+      parser.yy.locations.length > 1
+    ) {
+      const addColRefToVariableLocation = function(variableValue, colRef) {
+        // See if colref is actually an alias
+        if (colRef.length === 1 && colRef[0].name) {
+          parser.yy.locations.some(location => {
+            if (location.type === 'column' && location.alias === colRef[0].name) {
+              colRef = location.identifierChain;
+              return true;
+            }
+          });
+        }
+
+        for (let i = parser.yy.locations.length - 1; i > 0; i--) {
+          const location = parser.yy.locations[i];
+          if (location.type === 'variable' && location.value === variableValue) {
+            location.colRef = { identifierChain: colRef };
+            break;
+          }
+        }
+      };
+
+      if (/\${[^}]*}/.test(left.columnReference[0].name)) {
+        // left is variable
+        addColRefToVariableLocation(left.columnReference[0].name, right.columnReference);
+      } else if (/\${[^}]*}/.test(right.columnReference[0].name)) {
+        // right is variable
+        addColRefToVariableLocation(right.columnReference[0].name, left.columnReference);
+      }
+    }
+  };
+
+  parser.suggestDatabases = function(details) {
+    parser.yy.result.suggestDatabases = details || {};
+  };
+
+  parser.suggestHdfs = function(details) {
+    parser.yy.result.suggestHdfs = details || {};
+  };
+
+  parser.suggestValues = function(details) {
+    parser.yy.result.suggestValues = details || {};
+  };
+
+  parser.determineCase = function(text) {
+    if (!parser.yy.caseDetermined) {
+      parser.yy.lowerCase = text.toLowerCase() === text;
+      parser.yy.caseDetermined = true;
+    }
+  };
+
+  parser.handleQuotedValueWithCursor = function(lexer, yytext, yylloc, quoteChar) {
+    if (yytext.indexOf('\u2020') !== -1 || yytext.indexOf('\u2021') !== -1) {
+      parser.yy.partialCursor = yytext.indexOf('\u2021') !== -1;
+      const cursorIndex = parser.yy.partialCursor
+        ? yytext.indexOf('\u2021')
+        : yytext.indexOf('\u2020');
+      parser.yy.cursorFound = {
+        first_line: yylloc.first_line,
+        last_line: yylloc.last_line,
+        first_column: yylloc.first_column + cursorIndex,
+        last_column: yylloc.first_column + cursorIndex + 1
+      };
+      const remainder = yytext.substring(cursorIndex + 1);
+      const remainingQuotes = (lexer.upcomingInput().match(new RegExp(quoteChar, 'g')) || [])
+        .length;
+      if (remainingQuotes > 0 && (remainingQuotes & 1) !== 0) {
+        parser.yy.missingEndQuote = false;
+        lexer.input();
+      } else {
+        parser.yy.missingEndQuote = true;
+        lexer.unput(remainder);
+      }
+      lexer.popState();
+      return true;
+    }
+    return false;
+  };
+
+  let lexerModified = false;
+
+  /**
+   * Main parser function
+   */
+  parser.parseSql = function(beforeCursor, afterCursor, debug) {
+    // Jison counts CRLF as two lines in the locations
+    beforeCursor = beforeCursor.replace(/\r\n|\n\r/gm, '\n');
+    afterCursor = afterCursor.replace(/\r\n|\n\r/gm, '\n');
+    parser.yy.result = { locations: [] };
+    parser.yy.lowerCase = false;
+    parser.yy.locations = [];
+    parser.yy.definitions = [];
+    parser.yy.allLocations = [];
+    parser.yy.subQueries = [];
+    parser.yy.errors = [];
+    parser.yy.selectListAliases = [];
+    parser.yy.activeDialect = 'generic';
+
+    parser.yy.locationsStack = [];
+    parser.yy.primariesStack = [];
+    parser.yy.subQueriesStack = [];
+    parser.yy.resultStack = [];
+    parser.yy.selectListAliasesStack = [];
+
+    delete parser.yy.caseDetermined;
+    delete parser.yy.cursorFound;
+    delete parser.yy.partialCursor;
+
+    parser.prepareNewStatement();
+
+    const REASONABLE_SURROUNDING_LENGTH = 150000; // About 3000 lines before and after
+
+    if (beforeCursor.length > REASONABLE_SURROUNDING_LENGTH) {
+      if (beforeCursor.length - beforeCursor.lastIndexOf(';') > REASONABLE_SURROUNDING_LENGTH) {
+        // Bail out if the last complete statement is more than 150000 chars before
+        return {};
+      }
+      // Cut it at the first statement found within 150000 chars before
+      const lastReasonableChunk = beforeCursor.substring(
+        beforeCursor.length - REASONABLE_SURROUNDING_LENGTH
+      );
+      beforeCursor = lastReasonableChunk.substring(lastReasonableChunk.indexOf(';') + 1);
+    }
+
+    if (afterCursor.length > REASONABLE_SURROUNDING_LENGTH) {
+      if (afterCursor.length - afterCursor.indexOf(';') > REASONABLE_SURROUNDING_LENGTH) {
+        // No need to bail out for what's comes after, we can still get keyword completion
+        afterCursor = '';
+      } else {
+        // Cut it at the last statement found within 150000 chars after
+        const firstReasonableChunk = afterCursor.substring(0, REASONABLE_SURROUNDING_LENGTH);
+        afterCursor = firstReasonableChunk.substring(0, firstReasonableChunk.lastIndexOf(';'));
+      }
+    }
+
+    parser.yy.partialLengths = parser.identifyPartials(beforeCursor, afterCursor);
+
+    if (parser.yy.partialLengths.left > 0) {
+      beforeCursor = beforeCursor.substring(0, beforeCursor.length - parser.yy.partialLengths.left);
+    }
+
+    if (parser.yy.partialLengths.right > 0) {
+      afterCursor = afterCursor.substring(parser.yy.partialLengths.right);
+    }
+
+    let result;
+    try {
+      // Add |CURSOR| or |PARTIAL_CURSOR| to represent the different cursor states in the lexer
+      result = parser.parse(
+        beforeCursor +
+          (beforeCursor.length === 0 || /[\s(]$/.test(beforeCursor) ? ' \u2020 ' : '\u2021') +
+          afterCursor
+      );
+    } catch (err) {
+      // On any error try to at least return any existing result
+      if (typeof parser.yy.result === 'undefined') {
+        throw err;
+      }
+      if (debug) {
+        console.log(err);
+        console.error(err.stack);
+      }
+      result = parser.yy.result;
+    }
+    if (parser.yy.errors.length > 0) {
+      parser.yy.result.errors = parser.yy.errors;
+      if (debug) {
+        console.log(parser.yy.errors);
+      }
+    }
+    try {
+      linkTablePrimaries();
+      parser.commitLocations();
+      // Clean up and prioritize
+      prioritizeSuggestions();
+    } catch (err) {
+      if (debug) {
+        console.log(err);
+        console.error(err.stack);
+      }
+    }
+
+    parser.yy.allLocations.sort((a, b) => {
+      if (a.location.first_line !== b.location.first_line) {
+        return a.location.first_line - b.location.first_line;
+      }
+      if (a.location.first_column !== b.location.first_column) {
+        return a.location.first_column - b.location.first_column;
+      }
+      if (a.location.last_column !== b.location.last_column) {
+        return b.location.last_column - a.location.last_column;
+      }
+      return b.type.localeCompare(a.type);
+    });
+    parser.yy.result.locations = parser.yy.allLocations;
+    parser.yy.result.definitions = parser.yy.definitions;
+
+    parser.yy.result.locations.forEach(location => {
+      delete location.linked;
+    });
+    if (typeof parser.yy.result.suggestColumns !== 'undefined') {
+      delete parser.yy.result.suggestColumns.linked;
+    }
+
+    SIMPLE_TABLE_REF_SUGGESTIONS.forEach(suggestionType => {
+      if (typeof parser.yy.result[suggestionType] !== 'undefined') {
+        delete parser.yy.result[suggestionType].linked;
+      }
+    });
+
+    if (typeof parser.yy.result.colRef !== 'undefined') {
+      delete parser.yy.result.colRef.linked;
+    }
+    if (typeof parser.yy.result.suggestKeyValues !== 'undefined') {
+      delete parser.yy.result.suggestKeyValues.linked;
+    }
+
+    if (typeof result.error !== 'undefined' && typeof result.error.expected !== 'undefined') {
+      // Remove the cursor from expected tokens
+      result.error.expected = result.error.expected.filter(token => token.indexOf('CURSOR') === -1)
+    }
+
+    if (typeof result.error !== 'undefined' && result.error.recoverable) {
+      delete result.error;
+    }
+
+    // Adjust all the statement locations to include white space surrounding them
+    let lastStatementLocation = null;
+    result.locations.forEach(location => {
+      if (location.type === 'statement') {
+        if (lastStatementLocation === null) {
+          location.location.first_line = 1;
+          location.location.first_column = 1;
+        } else {
+          location.location.first_line = lastStatementLocation.location.last_line;
+          location.location.first_column = lastStatementLocation.location.last_column + 1;
+        }
+        lastStatementLocation = location;
+      }
+    });
+
+    return result;
+  };
+};
+
+const SYNTAX_PARSER_NOOP_FUNCTIONS = [
+  'addAsteriskLocation',
+  'addClauseLocation',
+  'addColRefIfExists',
+  'addColRefToVariableIfExists',
+  'addColumnAliasLocation',
+  'addColumnLocation',
+  'addCommonTableExpressions',
+  'addCteAliasLocation',
+  'addDatabaseLocation',
+  'addFileLocation',
+  'addFunctionLocation',
+  'addNewDatabaseLocation',
+  'addNewTableLocation',
+  'addStatementLocation',
+  'addStatementTypeLocation',
+  'addSubqueryAliasLocation',
+  'addTableAliasLocation',
+  'addTableLocation',
+  'addTablePrimary',
+  'addUnknownLocation',
+  'addVariableLocation',
+  'applyArgumentTypesToSuggestions',
+  'applyTypeToSuggestions',
+  'checkForKeywords',
+  'checkForSelectListKeywords',
+  'commitLocations',
+  'firstDefined',
+  'getSelectListKeywords',
+  'getSubQuery',
+  'getValueExpressionKeywords',
+  'identifyPartials',
+  'popQueryState',
+  'prepareNewStatement',
+  'pushQueryState',
+  'selectListNoTableSuggest',
+  'suggestAggregateFunctions',
+  'suggestAnalyticFunctions',
+  'suggestColRefKeywords',
+  'suggestColumns',
+  'suggestDatabases',
+  'suggestDdlAndDmlKeywords',
+  'suggestFileFormats',
+  'suggestFilters',
+  'suggestFunctions',
+  'suggestGroupBys',
+  'suggestHdfs',
+  'suggestIdentifiers',
+  'suggestJoinConditions',
+  'suggestJoins',
+  'suggestKeyValues',
+  'suggestKeywords',
+  'suggestOrderBys',
+  'suggestSelectListAliases',
+  'suggestTables',
+  'suggestTablesOrColumns',
+  'suggestValueExpressionKeywords',
+  'suggestValues',
+  'valueExpressionSuggest'
+];
+
+const SYNTAX_PARSER_NOOP = function() {};
+
+const initSyntaxParser = function(parser) {
+  // Noop functions for compatibility with the autocomplete parser as the grammar is shared
+  SYNTAX_PARSER_NOOP_FUNCTIONS.forEach(noopFn => {
+    parser[noopFn] = SYNTAX_PARSER_NOOP;
+  });
+
+  parser.yy.locations = [{}];
+
+  parser.determineCase = function(text) {
+    if (!parser.yy.caseDetermined) {
+      parser.yy.lowerCase = text.toLowerCase() === text;
+      parser.yy.caseDetermined = true;
+    }
+  };
+
+  parser.getKeywordsForOptionalsLR = function() {
+    return [];
+  };
+
+  parser.mergeSuggestKeywords = function() {
+    return {};
+  };
+
+  parser.getTypeKeywords = function() {
+    return [];
+  };
+
+  parser.getColumnDataTypeKeywords = function() {
+    return [];
+  };
+
+  parser.findCaseType = function() {
+    return { types: ['T'] };
+  };
+
+  parser.findReturnTypes = function() {
+    return ['T'];
+  };
+
+  parser.expandIdentifierChain = function() {
+    return [];
+  };
+
+  parser.createWeightedKeywords = function() {
+    return [];
+  };
+
+  parser.handleQuotedValueWithCursor = function(lexer, yytext, yylloc, quoteChar) {
+    if (yytext.indexOf('\u2020') !== -1 || yytext.indexOf('\u2021') !== -1) {
+      parser.yy.partialCursor = yytext.indexOf('\u2021') !== -1;
+      const cursorIndex = parser.yy.partialCursor
+        ? yytext.indexOf('\u2021')
+        : yytext.indexOf('\u2020');
+      parser.yy.cursorFound = {
+        first_line: yylloc.first_line,
+        last_line: yylloc.last_line,
+        first_column: yylloc.first_column + cursorIndex,
+        last_column: yylloc.first_column + cursorIndex + 1
+      };
+      const remainder = yytext.substring(cursorIndex + 1);
+      const remainingQuotes = (lexer.upcomingInput().match(new RegExp(quoteChar, 'g')) || [])
+        .length;
+      if (remainingQuotes > 0 && (remainingQuotes & 1) !== 0) {
+        parser.yy.missingEndQuote = false;
+        lexer.input();
+      } else {
+        parser.yy.missingEndQuote = true;
+        lexer.unput(remainder);
+      }
+      lexer.popState();
+      return true;
+    }
+    return false;
+  };
+
+  let lexerModified = false;
+
+  parser.yy.parseError = function(str, hash) {
+    parser.yy.error = hash;
+  };
+
+  const IGNORED_EXPECTED = {
+    ';': true,
+    '.': true,
+    EOF: true,
+    UNSIGNED_INTEGER: true,
+    UNSIGNED_INTEGER_E: true,
+    REGULAR_IDENTIFIER: true,
+    CURSOR: true,
+    PARTIAL_CURSOR: true,
+    HDFS_START_QUOTE: true,
+    HDFS_PATH: true,
+    HDFS_END_QUOTE: true,
+    COMPARISON_OPERATOR: true, // TODO: Expand in results when found
+    ARITHMETIC_OPERATOR: true, // TODO: Expand in results when found
+    VARIABLE_REFERENCE: true,
+    BACKTICK: true,
+    VALUE: true,
+    PARTIAL_VALUE: true,
+    SINGLE_QUOTE: true,
+    DOUBLE_QUOTE: true
+  };
+
+  const CLEAN_EXPECTED = {
+    BETWEEN_AND: 'AND',
+    OVERWRITE_DIRECTORY: 'OVERWRITE',
+    STORED_AS_DIRECTORIES: 'STORED',
+    LIKE_PARQUET: 'LIKE',
+    PARTITION_VALUE: 'PARTITION'
+  };
+
+  parser.parseSyntax = function(beforeCursor, afterCursor, debug) {
+    parser.yy.caseDetermined = false;
+    parser.yy.error = undefined;
+
+    parser.yy.latestTablePrimaries = [];
+    parser.yy.subQueries = [];
+    parser.yy.selectListAliases = [];
+    parser.yy.latestTablePrimaries = [];
+
+    parser.yy.activeDialect = 'generic';
+
+    // TODO: Find a way around throwing an exception when the parser finds a syntax error
+    try {
+      parser.yy.error = false;
+      parser.parse(beforeCursor + afterCursor);
+    } catch (err) {
+      if (debug) {
+        console.log(err);
+        console.error(err.stack);
+        console.log(parser.yy.error);
+      }
+    }
+
+    if (
+      parser.yy.error &&
+      (parser.yy.error.loc.last_column < beforeCursor.length ||
+        !beforeCursor.endsWith(parser.yy.error.text))
+    ) {
+      const weightedExpected = [];
+
+      const addedExpected = {};
+
+      const isLowerCase =
+        (parser.yy.caseDetermined && parser.yy.lowerCase) ||
+        parser.yy.error.text.toLowerCase() === parser.yy.error.text;
+
+      if (
+        parser.yy.error.expected.length === 2 &&
+        parser.yy.error.expected.indexOf("';'") !== -1 &&
+        parser.yy.error.expected.indexOf("'EOF'") !== -1
+      ) {
+        parser.yy.error.expected = [];
+        parser.yy.error.expectedStatementEnd = true;
+        return parser.yy.error;
+      }
+      for (let i = 0; i < parser.yy.error.expected.length; i++) {
+        let expected = parser.yy.error.expected[i];
+        // Strip away the surrounding ' chars
+        expected = expected.substring(1, expected.length - 1);
+        // TODO: Only suggest alphanumeric?
+        if (expected === 'REGULAR_IDENTIFIER') {
+          parser.yy.error.expectedIdentifier = true;
+          if (/^<[a-z]+>/.test(parser.yy.error.token)) {
+            const text = '`' + parser.yy.error.text + '`';
+            weightedExpected.push({
+              text: text,
+              distance: stringDistance(parser.yy.error.text, text, true)
+            });
+            parser.yy.error.possibleReserved = true;
+          }
+        } else if (!IGNORED_EXPECTED[expected] && /[a-z_]+/i.test(expected)) {
+          if (/^<[a-z]+>/.test(expected)) {
+            continue;
+          }
+          expected = CLEAN_EXPECTED[expected] || expected;
+          if (expected === parser.yy.error.text.toUpperCase()) {
+            // Can happen when the lexer entry for a rule contains multiple words like 'stored' in 'stored as parquet'
+            return false;
+          }
+          const text = isLowerCase ? expected.toLowerCase() : expected;
+          if (text && !addedExpected[text]) {
+            addedExpected[text] = true;
+            weightedExpected.push({
+              text: text,
+              distance: stringDistance(parser.yy.error.text, text, true)
+            });
+          }
+        }
+      }
+      if (weightedExpected.length === 0) {
+        parser.yy.error.expected = [];
+        parser.yy.error.incompleteStatement = true;
+        return parser.yy.error;
+      }
+      weightedExpected.sort((a, b) => {
+        if (a.distance === b.distance) {
+          return a.text.localeCompare(b.text);
+        }
+        return a.distance - b.distance;
+      });
+      parser.yy.error.expected = weightedExpected;
+      parser.yy.error.incompleteStatement = true;
+      return parser.yy.error;
+    } else if (parser.yy.error) {
+      parser.yy.error.expected = [];
+      parser.yy.error.incompleteStatement = true;
+      return parser.yy.error;
+    }
+    return false;
+  };
+};
+
+export default {
+  initSqlParser: initSqlParser,
+  initSyntaxParser: initSyntaxParser,
+  stringDistance: stringDistance
+};

Разница между файлами не показана из-за своего большого размера
+ 1 - 1
desktop/core/src/desktop/js/parse/sql/hive/hiveAutocompleteParser.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
desktop/core/src/desktop/js/parse/sql/hive/hiveSyntaxParser.js


+ 2505 - 0
desktop/core/src/desktop/js/parse/sql/hive/sqlParseSupport.js

@@ -0,0 +1,2505 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 { SqlFunctions } from 'sql/sqlFunctions';
+
+const identifierEquals = (a, b) =>
+  a &&
+  b &&
+  a
+    .replace(/^\s*`/, '')
+    .replace(/`\s*$/, '')
+    .toLowerCase() ===
+    b
+      .replace(/^\s*`/, '')
+      .replace(/`\s*$/, '')
+      .toLowerCase();
+
+// endsWith polyfill from hue_utils.js, needed as workers live in their own js environment
+if (!String.prototype.endsWith) {
+  String.prototype.endsWith = function(searchString, position) {
+    const subjectString = this.toString();
+    if (
+      typeof position !== 'number' ||
+      !isFinite(position) ||
+      Math.floor(position) !== position ||
+      position > subjectString.length
+    ) {
+      position = subjectString.length;
+    }
+    position -= searchString.length;
+    const lastIndex = subjectString.lastIndexOf(searchString, position);
+    return lastIndex !== -1 && lastIndex === position;
+  };
+}
+
+/**
+ * Calculates the Optimal String Alignment distance between two strings. Returns 0 when the strings are equal and the
+ * distance when not, distances is less than or equal to the length of the longest string.
+ *
+ * @param strA
+ * @param strB
+ * @param [ignoreCase]
+ * @returns {number} The similarity
+ */
+const stringDistance = function(strA, strB, ignoreCase) {
+  if (ignoreCase) {
+    strA = strA.toLowerCase();
+    strB = strB.toLowerCase();
+  }
+
+  // TODO: Consider other algorithms for performance
+  const strALength = strA.length;
+  const strBLength = strB.length;
+  if (strALength === 0) {
+    return strBLength;
+  }
+  if (strBLength === 0) {
+    return strALength;
+  }
+
+  const distances = new Array(strALength);
+
+  let cost, deletion, insertion, substitution, transposition;
+  for (let i = 0; i <= strALength; i++) {
+    distances[i] = new Array(strBLength);
+    distances[i][0] = i;
+    for (let j = 1; j <= strBLength; j++) {
+      if (!i) {
+        distances[0][j] = j;
+      } else {
+        cost = strA[i - 1] === strB[j - 1] ? 0 : 1;
+        deletion = distances[i - 1][j] + 1;
+        insertion = distances[i][j - 1] + 1;
+        substitution = distances[i - 1][j - 1] + cost;
+        if (deletion <= insertion && deletion <= substitution) {
+          distances[i][j] = deletion;
+        } else if (insertion <= deletion && insertion <= substitution) {
+          distances[i][j] = insertion;
+        } else {
+          distances[i][j] = substitution;
+        }
+
+        if (i > 1 && j > 1 && strA[i] === strB[j - 1] && strA[i - 1] === strB[j]) {
+          transposition = distances[i - 2][j - 2] + cost;
+          if (transposition < distances[i][j]) {
+            distances[i][j] = transposition;
+          }
+        }
+      }
+    }
+  }
+
+  return distances[strALength][strBLength];
+};
+
+const equalIgnoreCase = (a, b) => a && b && a.toLowerCase() === b.toLowerCase();
+
+const SIMPLE_TABLE_REF_SUGGESTIONS = [
+  'suggestJoinConditions',
+  'suggestAggregateFunctions',
+  'suggestFilters',
+  'suggestGroupBys',
+  'suggestOrderBys'
+];
+
+const initSqlParser = function(parser) {
+  parser.prepareNewStatement = function() {
+    linkTablePrimaries();
+    parser.commitLocations();
+
+    delete parser.yy.lateralViews;
+    delete parser.yy.latestCommonTableExpressions;
+    delete parser.yy.correlatedSubQuery;
+    parser.yy.subQueries = [];
+    parser.yy.selectListAliases = [];
+    parser.yy.latestTablePrimaries = [];
+
+    prioritizeSuggestions();
+  };
+
+  parser.yy.parseError = function(message, error) {
+    parser.yy.errors.push(error);
+    return message;
+  };
+
+  parser.addCommonTableExpressions = function(identifiers) {
+    parser.yy.result.commonTableExpressions = identifiers;
+    parser.yy.latestCommonTableExpressions = identifiers;
+  };
+
+  parser.isInSubquery = function() {
+    return !!parser.yy.primariesStack.length;
+  };
+
+  parser.pushQueryState = function() {
+    parser.yy.resultStack.push(parser.yy.result);
+    parser.yy.locationsStack.push(parser.yy.locations);
+    parser.yy.lateralViewsStack.push(parser.yy.lateralViews);
+    parser.yy.selectListAliasesStack.push(parser.yy.selectListAliases);
+    parser.yy.primariesStack.push(parser.yy.latestTablePrimaries);
+    parser.yy.subQueriesStack.push(parser.yy.subQueries);
+
+    parser.yy.result = {};
+    parser.yy.locations = [];
+    parser.yy.selectListAliases = []; // Not allowed in correlated sub-queries
+    parser.yy.lateralViews = []; // Not allowed in correlated sub-queries
+
+    if (parser.yy.correlatedSubQuery) {
+      parser.yy.latestTablePrimaries = parser.yy.latestTablePrimaries.concat();
+      parser.yy.subQueries = parser.yy.subQueries.concat();
+    } else {
+      parser.yy.latestTablePrimaries = [];
+      parser.yy.subQueries = [];
+    }
+  };
+
+  parser.popQueryState = function(subQuery) {
+    linkTablePrimaries();
+    parser.commitLocations();
+
+    if (Object.keys(parser.yy.result).length === 0) {
+      parser.yy.result = parser.yy.resultStack.pop();
+    } else {
+      parser.yy.resultStack.pop();
+    }
+    const oldSubQueries = parser.yy.subQueries;
+    parser.yy.subQueries = parser.yy.subQueriesStack.pop();
+    if (subQuery) {
+      if (oldSubQueries.length > 0) {
+        subQuery.subQueries = oldSubQueries;
+      }
+      parser.yy.subQueries.push(subQuery);
+    }
+
+    parser.yy.lateralViews = parser.yy.lateralViewsStack.pop();
+    parser.yy.latestTablePrimaries = parser.yy.primariesStack.pop();
+    parser.yy.locations = parser.yy.locationsStack.pop();
+    parser.yy.selectListAliases = parser.yy.selectListAliasesStack.pop();
+  };
+
+  parser.suggestSelectListAliases = function() {
+    if (
+      parser.yy.selectListAliases &&
+      parser.yy.selectListAliases.length > 0 &&
+      parser.yy.result.suggestColumns &&
+      (typeof parser.yy.result.suggestColumns.identifierChain === 'undefined' ||
+        parser.yy.result.suggestColumns.identifierChain.length === 0)
+    ) {
+      parser.yy.result.suggestColumnAliases = parser.yy.selectListAliases;
+    }
+  };
+
+  parser.mergeSuggestKeywords = function() {
+    let result = [];
+    Array.prototype.slice.call(arguments).forEach(suggestion => {
+      if (typeof suggestion !== 'undefined' && typeof suggestion.suggestKeywords !== 'undefined') {
+        result = result.concat(suggestion.suggestKeywords);
+      }
+    });
+    if (result.length > 0) {
+      return { suggestKeywords: result };
+    }
+    return {};
+  };
+
+  parser.suggestValueExpressionKeywords = function(valueExpression, extras) {
+    const expressionKeywords = parser.getValueExpressionKeywords(valueExpression, extras);
+    parser.suggestKeywords(expressionKeywords.suggestKeywords);
+    if (expressionKeywords.suggestColRefKeywords) {
+      parser.suggestColRefKeywords(expressionKeywords.suggestColRefKeywords);
+    }
+    if (valueExpression.lastType) {
+      parser.addColRefIfExists(valueExpression.lastType);
+    } else {
+      parser.addColRefIfExists(valueExpression);
+    }
+  };
+
+  parser.getSelectListKeywords = function(excludeAsterisk) {
+    let keywords = [{ value: 'CASE', weight: 450 }, 'EXISTS', 'FALSE', 'TRUE', 'NULL', 'NOT'];
+    if (!excludeAsterisk) {
+      keywords.push({ value: '*', weight: 10000 });
+    }
+    return keywords;
+  };
+
+  parser.getValueExpressionKeywords = function(valueExpression, extras) {
+    const types = valueExpression.lastType ? valueExpression.lastType.types : valueExpression.types;
+    // We could have valueExpression.columnReference to suggest based on column type
+    let keywords = [
+      '<',
+      '<=',
+      '<=>',
+      '<>',
+      '=',
+      '>',
+      '>=',
+      'BETWEEN',
+      'IN',
+      'IS NOT NULL',
+      'IS NULL',
+      'IS NOT TRUE',
+      'IS TRUE',
+      'IS NOT FALSE',
+      'IS FALSE',
+      'NOT BETWEEN',
+      'NOT IN'
+    ];
+    if (extras) {
+      keywords = keywords.concat(extras);
+    }
+    if (valueExpression.suggestKeywords) {
+      keywords = keywords.concat(valueExpression.suggestKeywords);
+    }
+    if (types.length === 1 && types[0] === 'COLREF') {
+      return {
+        suggestKeywords: keywords,
+        suggestColRefKeywords: {
+          BOOLEAN: ['AND', 'OR'],
+          NUMBER: ['+', '-', '*', '/', '%', 'DIV'],
+          STRING: ['LIKE', 'NOT LIKE', 'REGEXP', 'RLIKE']
+        }
+      };
+    }
+    if (
+      typeof SqlFunctions === 'undefined' ||
+      SqlFunctions.matchesType(parser.yy.activeDialect, ['BOOLEAN'], types)
+    ) {
+      keywords = keywords.concat(['AND', 'OR']);
+    }
+    if (
+      typeof SqlFunctions === 'undefined' ||
+      SqlFunctions.matchesType(parser.yy.activeDialect, ['NUMBER'], types)
+    ) {
+      keywords = keywords.concat(['+', '-', '*', '/', '%', 'DIV']);
+    }
+    if (
+      typeof SqlFunctions === 'undefined' ||
+      SqlFunctions.matchesType(parser.yy.activeDialect, ['STRING'], types)
+    ) {
+      keywords = keywords.concat(['LIKE', 'NOT LIKE', 'REGEXP', 'RLIKE']);
+    }
+    return { suggestKeywords: keywords };
+  };
+
+  parser.getTypeKeywords = function() {
+    return [
+      'BIGINT',
+      'BINARY',
+      'BOOLEAN',
+      'CHAR',
+      'DATE',
+      'DECIMAL',
+      'DOUBLE',
+      'DOUBLE PRECISION',
+      'FLOAT',
+      'INT',
+      'SMALLINT',
+      'TIMESTAMP',
+      'STRING',
+      'TINYINT',
+      'VARCHAR'
+    ];
+  };
+
+  parser.getColumnDataTypeKeywords = function() {
+    return parser.getTypeKeywords().concat(['ARRAY<>', 'MAP<>', 'STRUCT<>', 'UNIONTYPE<>']);
+  };
+
+  parser.addColRefIfExists = function(valueExpression) {
+    if (valueExpression.columnReference) {
+      parser.yy.result.colRef = { identifierChain: valueExpression.columnReference };
+    }
+  };
+
+  parser.selectListNoTableSuggest = function(selectListEdit, hasDistinctOrAll) {
+    if (selectListEdit.cursorAtStart) {
+      let keywords = parser.getSelectListKeywords();
+      if (!hasDistinctOrAll) {
+        keywords = keywords.concat([{ value: 'ALL', weight: 2 }, { value: 'DISTINCT', weight: 2 }]);
+      }
+      parser.suggestKeywords(keywords);
+    } else {
+      parser.checkForKeywords(selectListEdit);
+    }
+    if (selectListEdit.suggestFunctions) {
+      parser.suggestFunctions();
+    }
+    if (selectListEdit.suggestColumns) {
+      parser.suggestColumns();
+    }
+    if (
+      selectListEdit.suggestAggregateFunctions &&
+      (!hasDistinctOrAll || hasDistinctOrAll === 'ALL')
+    ) {
+      parser.suggestAggregateFunctions();
+      parser.suggestAnalyticFunctions();
+    }
+  };
+
+  parser.suggestJoinConditions = function(details) {
+    parser.yy.result.suggestJoinConditions = details || {};
+    if (parser.yy.latestTablePrimaries && !parser.yy.result.suggestJoinConditions.tablePrimaries) {
+      parser.yy.result.suggestJoinConditions.tablePrimaries = parser.yy.latestTablePrimaries.concat();
+    }
+  };
+
+  parser.suggestJoins = function(details) {
+    parser.yy.result.suggestJoins = details || {};
+  };
+
+  parser.valueExpressionSuggest = function(oppositeValueExpression, operator) {
+    if (oppositeValueExpression && oppositeValueExpression.columnReference) {
+      parser.suggestValues();
+      parser.yy.result.colRef = { identifierChain: oppositeValueExpression.columnReference };
+    }
+    parser.suggestColumns();
+    parser.suggestFunctions();
+    let keywords = [
+      { value: 'CASE', weight: 450 },
+      { value: 'FALSE', weight: 450 },
+      { value: 'NULL', weight: 450 },
+      { value: 'TRUE', weight: 450 }
+    ];
+    keywords = keywords.concat(['EXISTS', 'NOT']);
+    if (oppositeValueExpression && oppositeValueExpression.types[0] === 'NUMBER') {
+      parser.applyTypeToSuggestions(['NUMBER']);
+    }
+    parser.suggestKeywords(keywords);
+  };
+
+  parser.applyTypeToSuggestions = function(types) {
+    if (types[0] === 'BOOLEAN') {
+      return;
+    }
+    if (parser.yy.result.suggestFunctions && !parser.yy.result.suggestFunctions.types) {
+      parser.yy.result.suggestFunctions.types = types;
+    }
+    if (parser.yy.result.suggestColumns && !parser.yy.result.suggestColumns.types) {
+      parser.yy.result.suggestColumns.types = types;
+    }
+  };
+
+  parser.findCaseType = function(whenThenList) {
+    const types = {};
+    whenThenList.caseTypes.forEach(valueExpression => {
+      valueExpression.types.forEach(type => {
+        types[type] = true;
+      });
+    });
+    if (Object.keys(types).length === 1) {
+      return { types: [Object.keys(types)[0]] };
+    }
+    return { types: ['T'] };
+  };
+
+  parser.findReturnTypes = function(functionName) {
+    return typeof SqlFunctions === 'undefined'
+      ? ['T']
+      : SqlFunctions.getReturnTypes(parser.yy.activeDialect, functionName.toLowerCase());
+  };
+
+  parser.applyArgumentTypesToSuggestions = function(functionName, position) {
+    const foundArguments =
+      typeof SqlFunctions === 'undefined'
+        ? ['T']
+        : SqlFunctions.getArgumentTypes(
+            parser.yy.activeDialect,
+            functionName.toLowerCase(),
+            position
+          );
+    if (foundArguments.length === 0 && parser.yy.result.suggestColumns) {
+      delete parser.yy.result.suggestColumns;
+      delete parser.yy.result.suggestKeyValues;
+      delete parser.yy.result.suggestValues;
+      delete parser.yy.result.suggestFunctions;
+      delete parser.yy.result.suggestIdentifiers;
+      delete parser.yy.result.suggestKeywords;
+    } else {
+      parser.applyTypeToSuggestions(foundArguments);
+    }
+  };
+
+  parser.commitLocations = function() {
+    if (parser.yy.locations.length === 0) {
+      return;
+    }
+
+    let tablePrimaries = parser.yy.latestTablePrimaries;
+
+    let i = parser.yy.locations.length;
+
+    while (i--) {
+      const location = parser.yy.locations[i];
+      if (location.type === 'variable' && location.colRef) {
+        parser.expandIdentifierChain({
+          wrapper: location.colRef,
+          tablePrimaries: tablePrimaries,
+          isColumnWrapper: true
+        });
+        delete location.colRef.linked;
+      }
+
+      if (location.type === 'unknown') {
+        if (
+          typeof location.identifierChain !== 'undefined' &&
+          location.identifierChain.length > 0 &&
+          location.identifierChain.length <= 2 &&
+          tablePrimaries
+        ) {
+          let found = tablePrimaries.filter(primary => {
+            return (
+              equalIgnoreCase(primary.alias, location.identifierChain[0].name) ||
+              (primary.identifierChain &&
+                equalIgnoreCase(primary.identifierChain[0].name, location.identifierChain[0].name))
+            );
+          });
+          if (!found.length && location.firstInChain) {
+            found = tablePrimaries.filter(primary => {
+              return (
+                !primary.alias &&
+                primary.identifierChain &&
+                equalIgnoreCase(
+                  primary.identifierChain[primary.identifierChain.length - 1].name,
+                  location.identifierChain[0].name
+                )
+              );
+            });
+          }
+
+          if (found.length) {
+            if (
+              found[0].identifierChain.length > 1 &&
+              location.identifierChain.length === 1 &&
+              equalIgnoreCase(found[0].identifierChain[0].name, location.identifierChain[0].name)
+            ) {
+              location.type = 'database';
+            } else if (
+              found[0].alias &&
+              equalIgnoreCase(location.identifierChain[0].name, found[0].alias) &&
+              location.identifierChain.length > 1
+            ) {
+              location.type = 'column';
+              parser.expandIdentifierChain({
+                tablePrimaries: tablePrimaries,
+                wrapper: location,
+                anyOwner: true
+              });
+            } else if (
+              !found[0].alias &&
+              found[0].identifierChain &&
+              equalIgnoreCase(
+                location.identifierChain[0].name,
+                found[0].identifierChain[found[0].identifierChain.length - 1].name
+              ) &&
+              location.identifierChain.length > 1
+            ) {
+              location.type = 'column';
+              parser.expandIdentifierChain({
+                tablePrimaries: tablePrimaries,
+                wrapper: location,
+                anyOwner: true
+              });
+            } else {
+              location.type = 'table';
+              parser.expandIdentifierChain({
+                tablePrimaries: tablePrimaries,
+                wrapper: location,
+                anyOwner: true
+              });
+            }
+          } else if (parser.yy.subQueries) {
+            found = parser.yy.subQueries.filter(subQuery => {
+              return equalIgnoreCase(subQuery.alias, location.identifierChain[0].name);
+            });
+            if (found.length > 0) {
+              location.type = 'subQuery';
+              location.identifierChain = [{ subQuery: found[0].alias }];
+            }
+          }
+        }
+      }
+
+      if (location.type === 'asterisk' && !location.linked) {
+        if (tablePrimaries && tablePrimaries.length > 0) {
+          location.tables = [];
+          location.linked = false;
+          if (!location.identifierChain) {
+            location.identifierChain = [{ asterisk: true }];
+          }
+          parser.expandIdentifierChain({
+            tablePrimaries: tablePrimaries,
+            wrapper: location,
+            anyOwner: false
+          });
+          if (location.tables.length === 0) {
+            parser.yy.locations.splice(i, 1);
+          }
+        } else {
+          parser.yy.locations.splice(i, 1);
+        }
+      }
+
+      if (
+        location.type === 'table' &&
+        typeof location.identifierChain !== 'undefined' &&
+        location.identifierChain.length === 1 &&
+        location.identifierChain[0].name
+      ) {
+        // Could be a cte reference
+        parser.yy.locations.some(otherLocation => {
+          if (
+            otherLocation.type === 'alias' &&
+            otherLocation.source === 'cte' &&
+            identifierEquals(otherLocation.alias, location.identifierChain[0].name)
+          ) {
+            // TODO: Possibly add the other location if we want to show the link in the future.
+            //       i.e. highlight select definition on hover over alias, also for subquery references.
+            location.type = 'alias';
+            location.target = 'cte';
+            location.alias = location.identifierChain[0].name;
+            delete location.identifierChain;
+            return true;
+          }
+        });
+      }
+
+      if (
+        location.type === 'table' &&
+        (typeof location.identifierChain === 'undefined' || location.identifierChain.length === 0)
+      ) {
+        parser.yy.locations.splice(i, 1);
+      }
+
+      if (location.type === 'unknown') {
+        location.type = 'column';
+      }
+
+      // A column location might refer to a previously defined alias, i.e. last 'foo' in "SELECT cast(id AS int) foo FROM tbl ORDER BY foo;"
+      if (location.type === 'column') {
+        for (let j = i - 1; j >= 0; j--) {
+          const otherLocation = parser.yy.locations[j];
+          if (
+            otherLocation.type === 'alias' &&
+            otherLocation.source === 'column' &&
+            location.identifierChain &&
+            location.identifierChain.length === 1 &&
+            location.identifierChain[0].name &&
+            otherLocation.alias &&
+            location.identifierChain[0].name.toLowerCase() === otherLocation.alias.toLowerCase()
+          ) {
+            location.type = 'alias';
+            location.source = 'column';
+            location.alias = location.identifierChain[0].name;
+            delete location.identifierChain;
+            location.parentLocation = otherLocation.parentLocation;
+            break;
+          }
+        }
+      }
+
+      if (location.type === 'column') {
+        if (!location.linked) {
+          location.identifierChain = parser.expandLateralViews(
+            parser.yy.lateralViews,
+            location.identifierChain
+          );
+        }
+
+        const initialIdentifierChain = location.identifierChain
+          ? location.identifierChain.concat()
+          : undefined;
+
+        parser.expandIdentifierChain({
+          tablePrimaries: tablePrimaries,
+          wrapper: location,
+          anyOwner: true,
+          isColumnWrapper: true,
+          isColumnLocation: true
+        });
+
+        if (typeof location.identifierChain === 'undefined') {
+          parser.yy.locations.splice(i, 1);
+        } else if (
+          location.identifierChain.length === 0 &&
+          initialIdentifierChain &&
+          initialIdentifierChain.length === 1
+        ) {
+          // This is for the case "SELECT tblOrColName FROM db.tblOrColName";
+          location.identifierChain = initialIdentifierChain;
+        }
+      }
+      if (location.type === 'column' && location.identifierChain) {
+        if (location.identifierChain.length > 1 && location.tables && location.tables.length > 0) {
+          location.type = 'complex';
+        }
+      }
+      delete location.firstInChain;
+      if (location.type !== 'column' && location.type !== 'complex') {
+        delete location.qualified;
+      } else if (typeof location.qualified === 'undefined') {
+        location.qualified = false;
+      }
+    }
+
+    if (parser.yy.locations.length > 0) {
+      parser.yy.allLocations = parser.yy.allLocations.concat(parser.yy.locations);
+      parser.yy.locations = [];
+    }
+  };
+
+  const prioritizeSuggestions = function() {
+    parser.yy.result.lowerCase = parser.yy.lowerCase || false;
+
+    const cteIndex = {};
+
+    if (typeof parser.yy.latestCommonTableExpressions !== 'undefined') {
+      parser.yy.latestCommonTableExpressions.forEach(cte => {
+        cteIndex[cte.alias.toLowerCase()] = cte;
+      });
+    }
+
+    SIMPLE_TABLE_REF_SUGGESTIONS.forEach(suggestionType => {
+      if (
+        suggestionType !== 'suggestAggregateFunctions' &&
+        typeof parser.yy.result[suggestionType] !== 'undefined' &&
+        parser.yy.result[suggestionType].tables.length === 0
+      ) {
+        delete parser.yy.result[suggestionType];
+      } else if (
+        typeof parser.yy.result[suggestionType] !== 'undefined' &&
+        typeof parser.yy.result[suggestionType].tables !== 'undefined'
+      ) {
+        for (let i = parser.yy.result[suggestionType].tables.length - 1; i >= 0; i--) {
+          const table = parser.yy.result[suggestionType].tables[i];
+          if (
+            table.identifierChain.length === 1 &&
+            typeof table.identifierChain[0].name !== 'undefined' &&
+            typeof cteIndex[table.identifierChain[0].name.toLowerCase()] !== 'undefined'
+          ) {
+            parser.yy.result[suggestionType].tables.splice(i, 1);
+          }
+        }
+      }
+    });
+
+    if (typeof parser.yy.result.colRef !== 'undefined') {
+      if (
+        !parser.yy.result.colRef.linked ||
+        typeof parser.yy.result.colRef.identifierChain === 'undefined' ||
+        parser.yy.result.colRef.identifierChain.length === 0
+      ) {
+        delete parser.yy.result.colRef;
+        if (typeof parser.yy.result.suggestColRefKeywords !== 'undefined') {
+          Object.keys(parser.yy.result.suggestColRefKeywords).forEach(type => {
+            parser.yy.result.suggestKeywords = parser.yy.result.suggestKeywords.concat(
+              parser.createWeightedKeywords(parser.yy.result.suggestColRefKeywords[type], -1)
+            );
+          });
+          delete parser.yy.result.suggestColRefKeywords;
+        }
+        if (
+          parser.yy.result.suggestColumns &&
+          parser.yy.result.suggestColumns.types.length === 1 &&
+          parser.yy.result.suggestColumns.types[0] === 'COLREF'
+        ) {
+          parser.yy.result.suggestColumns.types = ['T'];
+        }
+        delete parser.yy.result.suggestValues;
+      }
+    }
+
+    if (typeof parser.yy.result.colRef !== 'undefined') {
+      if (
+        !parser.yy.result.suggestValues &&
+        !parser.yy.result.suggestColRefKeywords &&
+        (!parser.yy.result.suggestColumns || parser.yy.result.suggestColumns.types[0] !== 'COLREF')
+      ) {
+        delete parser.yy.result.colRef;
+      }
+    }
+    if (
+      typeof parser.yy.result.suggestIdentifiers !== 'undefined' &&
+      parser.yy.result.suggestIdentifiers.length > 0
+    ) {
+      delete parser.yy.result.suggestTables;
+      delete parser.yy.result.suggestDatabases;
+    }
+    if (typeof parser.yy.result.suggestColumns !== 'undefined') {
+      const suggestColumns = parser.yy.result.suggestColumns;
+      if (typeof suggestColumns.tables === 'undefined' || suggestColumns.tables.length === 0) {
+        delete parser.yy.result.suggestColumns;
+        delete parser.yy.result.subQueries;
+      } else {
+        delete parser.yy.result.suggestTables;
+        delete parser.yy.result.suggestDatabases;
+
+        suggestColumns.tables.forEach(table => {
+          if (
+            typeof table.identifierChain !== 'undefined' &&
+            table.identifierChain.length === 1 &&
+            typeof table.identifierChain[0].name !== 'undefined'
+          ) {
+            const cte = cteIndex[table.identifierChain[0].name.toLowerCase()];
+            if (typeof cte !== 'undefined') {
+              delete table.identifierChain[0].name;
+              table.identifierChain[0].cte = cte.alias;
+            }
+          } else if (typeof table.identifierChain === 'undefined' && table.subQuery) {
+            table.identifierChain = [{ subQuery: table.subQuery }];
+            delete table.subQuery;
+          }
+        });
+
+        if (
+          typeof suggestColumns.identifierChain !== 'undefined' &&
+          suggestColumns.identifierChain.length === 0
+        ) {
+          delete suggestColumns.identifierChain;
+        }
+      }
+    } else {
+      delete parser.yy.result.subQueries;
+    }
+
+    if (typeof parser.yy.result.suggestJoinConditions !== 'undefined') {
+      if (
+        typeof parser.yy.result.suggestJoinConditions.tables === 'undefined' ||
+        parser.yy.result.suggestJoinConditions.tables.length === 0
+      ) {
+        delete parser.yy.result.suggestJoinConditions;
+      }
+    }
+
+    if (
+      typeof parser.yy.result.suggestTables !== 'undefined' &&
+      typeof parser.yy.result.commonTableExpressions !== 'undefined'
+    ) {
+      const ctes = [];
+      parser.yy.result.commonTableExpressions.forEach(cte => {
+        const suggestion = { name: cte.alias };
+        if (parser.yy.result.suggestTables.prependFrom) {
+          suggestion.prependFrom = true;
+        }
+        if (parser.yy.result.suggestTables.prependQuestionMark) {
+          suggestion.prependQuestionMark = true;
+        }
+        ctes.push(suggestion);
+      });
+      if (ctes.length > 0) {
+        parser.yy.result.suggestCommonTableExpressions = ctes;
+      }
+    }
+  };
+
+  parser.identifyPartials = function(beforeCursor, afterCursor) {
+    const beforeMatch = beforeCursor.match(/[0-9a-zA-Z_]*$/);
+    const afterMatch = afterCursor.match(/^[0-9a-zA-Z_]*(?:\((?:[^)]*\))?)?/);
+    return {
+      left: beforeMatch ? beforeMatch[0].length : 0,
+      right: afterMatch ? afterMatch[0].length : 0
+    };
+  };
+
+  parser.expandLateralViews = function(lateralViews, originalIdentifierChain, columnSuggestion) {
+    let identifierChain = originalIdentifierChain.concat(); // Clone in case it's re-used
+    let firstIdentifier = identifierChain[0];
+    if (typeof lateralViews !== 'undefined') {
+      lateralViews
+        .concat()
+        .reverse()
+        .forEach(lateralView => {
+          if (!lateralView.udtf.expression.columnReference) {
+            return;
+          }
+          if (
+            equalIgnoreCase(firstIdentifier.name, lateralView.tableAlias) &&
+            identifierChain.length > 1
+          ) {
+            identifierChain.shift();
+            firstIdentifier = identifierChain[0];
+            if (columnSuggestion) {
+              delete parser.yy.result.suggestKeywords;
+            }
+          } else if (
+            equalIgnoreCase(firstIdentifier.name, lateralView.tableAlias) &&
+            identifierChain.length === 1 &&
+            typeof parser.yy.result.suggestColumns !== 'undefined'
+          ) {
+            if (columnSuggestion) {
+              if (typeof parser.yy.result.suggestIdentifiers === 'undefined') {
+                parser.yy.result.suggestIdentifiers = [];
+              }
+              lateralView.columnAliases.forEach(columnAlias => {
+                parser.yy.result.suggestIdentifiers.push({ name: columnAlias, type: 'alias' });
+              });
+              delete parser.yy.result.suggestColumns;
+              delete parser.yy.result.suggestKeywords;
+            }
+            return identifierChain;
+          }
+          if (lateralView.columnAliases.indexOf(firstIdentifier.name) !== -1) {
+            if (
+              lateralView.columnAliases.length === 2 &&
+              lateralView.udtf.function.toLowerCase() === 'explode' &&
+              equalIgnoreCase(firstIdentifier.name, lateralView.columnAliases[0])
+            ) {
+              identifierChain[0] = { name: 'key' };
+            } else if (
+              lateralView.columnAliases.length === 2 &&
+              lateralView.udtf.function.toLowerCase() === 'explode' &&
+              equalIgnoreCase(firstIdentifier.name, lateralView.columnAliases[1])
+            ) {
+              identifierChain[0] = { name: 'value' };
+            } else {
+              identifierChain[0] = { name: 'item' };
+            }
+            identifierChain = lateralView.udtf.expression.columnReference.concat(identifierChain);
+            firstIdentifier = identifierChain[0];
+          }
+        });
+    }
+    return identifierChain;
+  };
+
+  const addCleanTablePrimary = function(tables, tablePrimary) {
+    if (tablePrimary.alias) {
+      tables.push({ alias: tablePrimary.alias, identifierChain: tablePrimary.identifierChain });
+    } else {
+      tables.push({ identifierChain: tablePrimary.identifierChain });
+    }
+  };
+
+  parser.expandIdentifierChain = function(options) {
+    const wrapper = options.wrapper;
+    const anyOwner = options.anyOwner;
+    const isColumnWrapper = options.isColumnWrapper;
+    const isColumnLocation = options.isColumnLocation;
+    let tablePrimaries = options.tablePrimaries || parser.yy.latestTablePrimaries;
+
+    if (typeof wrapper.identifierChain === 'undefined' || typeof tablePrimaries === 'undefined') {
+      return;
+    }
+    let identifierChain = wrapper.identifierChain.concat();
+
+    if (tablePrimaries.length === 0) {
+      delete wrapper.identifierChain;
+      return;
+    }
+
+    if (!anyOwner) {
+      tablePrimaries = filterTablePrimariesForOwner(tablePrimaries, wrapper.owner);
+    }
+
+    if (identifierChain.length > 0 && identifierChain[identifierChain.length - 1].asterisk) {
+      const tables = [];
+      tablePrimaries.forEach(tablePrimary => {
+        if (identifierChain.length > 1 && !tablePrimary.subQueryAlias) {
+          if (
+            identifierChain.length === 2 &&
+            equalIgnoreCase(tablePrimary.alias, identifierChain[0].name)
+          ) {
+            addCleanTablePrimary(tables, tablePrimary);
+          } else if (
+            identifierChain.length === 2 &&
+            equalIgnoreCase(tablePrimary.identifierChain[0].name, identifierChain[0].name)
+          ) {
+            addCleanTablePrimary(tables, tablePrimary);
+          } else if (
+            identifierChain.length === 3 &&
+            tablePrimary.identifierChain.length > 1 &&
+            equalIgnoreCase(tablePrimary.identifierChain[0].name, identifierChain[0].name) &&
+            equalIgnoreCase(tablePrimary.identifierChain[1].name, identifierChain[1].name)
+          ) {
+            addCleanTablePrimary(tables, tablePrimary);
+          }
+        } else if (tablePrimary.subQueryAlias) {
+          tables.push({ identifierChain: [{ subQuery: tablePrimary.subQueryAlias }] });
+        } else {
+          addCleanTablePrimary(tables, tablePrimary);
+        }
+      });
+      // Possible Joins
+      if (tables.length > 0) {
+        wrapper.tables = tables;
+        delete wrapper.identifierChain;
+        return;
+      }
+    }
+
+    // Expand exploded views in the identifier chain
+    if (identifierChain.length > 0) {
+      identifierChain = parser.expandLateralViews(parser.yy.lateralViews, identifierChain);
+      wrapper.identifierChain = identifierChain;
+    }
+
+    // IdentifierChain contains a possibly started identifier or empty, example: a.b.c = ['a', 'b', 'c']
+    // Reduce the tablePrimaries to the one that matches the first identifier if found
+    let foundPrimary;
+    let doubleMatch = false;
+    let aliasMatch = false;
+    if (identifierChain.length > 0) {
+      for (let i = 0; i < tablePrimaries.length; i++) {
+        if (tablePrimaries[i].subQueryAlias) {
+          if (equalIgnoreCase(tablePrimaries[i].subQueryAlias, identifierChain[0].name)) {
+            foundPrimary = tablePrimaries[i];
+          }
+        } else if (equalIgnoreCase(tablePrimaries[i].alias, identifierChain[0].name)) {
+          foundPrimary = tablePrimaries[i];
+          aliasMatch = true;
+          break;
+        } else if (
+          tablePrimaries[i].identifierChain.length > 1 &&
+          identifierChain.length > 1 &&
+          equalIgnoreCase(tablePrimaries[i].identifierChain[0].name, identifierChain[0].name) &&
+          equalIgnoreCase(tablePrimaries[i].identifierChain[1].name, identifierChain[1].name)
+        ) {
+          foundPrimary = tablePrimaries[i];
+          doubleMatch = true;
+          break;
+        } else if (
+          !foundPrimary &&
+          equalIgnoreCase(tablePrimaries[i].identifierChain[0].name, identifierChain[0].name) &&
+          identifierChain.length > (isColumnLocation ? 1 : 0)
+        ) {
+          foundPrimary = tablePrimaries[i];
+          // No break as first two can still match.
+        } else if (
+          !foundPrimary &&
+          tablePrimaries[i].identifierChain.length > 1 &&
+          !tablePrimaries[i].alias &&
+          equalIgnoreCase(
+            tablePrimaries[i].identifierChain[tablePrimaries[i].identifierChain.length - 1].name,
+            identifierChain[0].name
+          )
+        ) {
+          // This is for the case SELECT baa. FROM bla.baa, blo.boo;
+          foundPrimary = tablePrimaries[i];
+          break;
+        }
+      }
+    }
+
+    if (foundPrimary) {
+      identifierChain.shift();
+      if (doubleMatch) {
+        identifierChain.shift();
+      }
+    } else if (tablePrimaries.length === 1 && !isColumnWrapper) {
+      foundPrimary = tablePrimaries[0];
+    }
+
+    if (foundPrimary) {
+      if (isColumnWrapper) {
+        wrapper.identifierChain = identifierChain;
+        if (foundPrimary.subQueryAlias) {
+          wrapper.tables = [{ subQuery: foundPrimary.subQueryAlias }];
+        } else if (foundPrimary.alias) {
+          if (!isColumnLocation && isColumnWrapper && aliasMatch) {
+            // TODO: add alias on table in suggestColumns (needs support in sqlAutocomplete3.js)
+            // the case is: SELECT cu.| FROM customers cu;
+            // This prevents alias from being added automatically in sqlAutocompleter.js
+            wrapper.tables = [{ identifierChain: foundPrimary.identifierChain }];
+          } else {
+            wrapper.tables = [
+              { identifierChain: foundPrimary.identifierChain, alias: foundPrimary.alias }
+            ];
+          }
+        } else {
+          wrapper.tables = [{ identifierChain: foundPrimary.identifierChain }];
+        }
+      } else {
+        if (foundPrimary.subQueryAlias) {
+          identifierChain.unshift({ subQuery: foundPrimary.subQueryAlias });
+        } else {
+          identifierChain = foundPrimary.identifierChain.concat(identifierChain);
+        }
+        if (wrapper.tables) {
+          wrapper.tables.push({ identifierChain: identifierChain });
+          delete wrapper.identifierChain;
+        } else {
+          wrapper.identifierChain = identifierChain;
+        }
+      }
+    } else {
+      if (isColumnWrapper) {
+        wrapper.tables = [];
+      }
+      tablePrimaries.forEach(tablePrimary => {
+        const targetTable = tablePrimary.subQueryAlias
+          ? { subQuery: tablePrimary.subQueryAlias }
+          : { identifierChain: tablePrimary.identifierChain };
+        if (tablePrimary.alias) {
+          targetTable.alias = tablePrimary.alias;
+        }
+        if (wrapper.tables) {
+          wrapper.tables.push(targetTable);
+        }
+      });
+    }
+    delete wrapper.owner;
+    wrapper.linked = true;
+  };
+
+  const suggestLateralViewAliasesAsIdentifiers = function() {
+    if (typeof parser.yy.lateralViews === 'undefined' || parser.yy.lateralViews.length === 0) {
+      return;
+    }
+    if (typeof parser.yy.result.suggestIdentifiers === 'undefined') {
+      parser.yy.result.suggestIdentifiers = [];
+    }
+    parser.yy.lateralViews.forEach(lateralView => {
+      if (typeof lateralView.tableAlias !== 'undefined') {
+        parser.yy.result.suggestIdentifiers.push({
+          name: lateralView.tableAlias + '.',
+          type: 'alias'
+        });
+      }
+      lateralView.columnAliases.forEach(columnAlias => {
+        parser.yy.result.suggestIdentifiers.push({ name: columnAlias, type: 'alias' });
+      });
+    });
+    if (parser.yy.result.suggestIdentifiers.length === 0) {
+      delete parser.yy.result.suggestIdentifiers;
+    }
+  };
+
+  const filterTablePrimariesForOwner = function(tablePrimaries, owner) {
+    const result = [];
+    tablePrimaries.forEach(primary => {
+      if (typeof owner === 'undefined' && typeof primary.owner === 'undefined') {
+        result.push(primary);
+      } else if (owner === primary.owner) {
+        result.push(primary);
+      }
+    });
+    return result;
+  };
+
+  const convertTablePrimariesToSuggestions = function(tablePrimaries) {
+    const tables = [];
+    const identifiers = [];
+    tablePrimaries.forEach(tablePrimary => {
+      if (tablePrimary.identifierChain && tablePrimary.identifierChain.length > 0) {
+        const table = { identifierChain: tablePrimary.identifierChain };
+        if (tablePrimary.alias) {
+          table.alias = tablePrimary.alias;
+          identifiers.push({ name: table.alias + '.', type: 'alias' });
+        } else {
+          const lastIdentifier =
+            tablePrimary.identifierChain[tablePrimary.identifierChain.length - 1];
+          if (typeof lastIdentifier.name !== 'undefined') {
+            identifiers.push({ name: lastIdentifier.name + '.', type: 'table' });
+          } else if (typeof lastIdentifier.subQuery !== 'undefined') {
+            identifiers.push({ name: lastIdentifier.subQuery + '.', type: 'sub-query' });
+          }
+        }
+        tables.push(table);
+      } else if (tablePrimary.subQueryAlias) {
+        identifiers.push({ name: tablePrimary.subQueryAlias + '.', type: 'sub-query' });
+        tables.push({ identifierChain: [{ subQuery: tablePrimary.subQueryAlias }] });
+      }
+    });
+    if (identifiers.length > 0) {
+      if (typeof parser.yy.result.suggestIdentifiers === 'undefined') {
+        parser.yy.result.suggestIdentifiers = identifiers;
+      } else {
+        parser.yy.result.suggestIdentifiers = identifiers.concat(
+          parser.yy.result.suggestIdentifiers
+        );
+      }
+    }
+    parser.yy.result.suggestColumns.tables = tables;
+    if (
+      parser.yy.result.suggestColumns.identifierChain &&
+      parser.yy.result.suggestColumns.identifierChain.length === 0
+    ) {
+      delete parser.yy.result.suggestColumns.identifierChain;
+    }
+    parser.yy.result.suggestColumns.linked = true;
+  };
+
+  const linkTablePrimaries = function() {
+    if (!parser.yy.cursorFound || typeof parser.yy.latestTablePrimaries === 'undefined') {
+      return;
+    }
+
+    SIMPLE_TABLE_REF_SUGGESTIONS.forEach(suggestionType => {
+      if (
+        typeof parser.yy.result[suggestionType] !== 'undefined' &&
+        parser.yy.result[suggestionType].tablePrimaries &&
+        !parser.yy.result[suggestionType].linked
+      ) {
+        parser.yy.result[suggestionType].tables = [];
+        parser.yy.result[suggestionType].tablePrimaries.forEach(tablePrimary => {
+          if (!tablePrimary.subQueryAlias) {
+            parser.yy.result[suggestionType].tables.push(
+              tablePrimary.alias
+                ? {
+                    identifierChain: tablePrimary.identifierChain.concat(),
+                    alias: tablePrimary.alias
+                  }
+                : { identifierChain: tablePrimary.identifierChain.concat() }
+            );
+          }
+        });
+        delete parser.yy.result[suggestionType].tablePrimaries;
+        parser.yy.result[suggestionType].linked = true;
+      }
+    });
+
+    if (
+      typeof parser.yy.result.suggestColumns !== 'undefined' &&
+      !parser.yy.result.suggestColumns.linked
+    ) {
+      const tablePrimaries = filterTablePrimariesForOwner(
+        parser.yy.latestTablePrimaries,
+        parser.yy.result.suggestColumns.owner
+      );
+      if (!parser.yy.result.suggestColumns.tables) {
+        parser.yy.result.suggestColumns.tables = [];
+      }
+      if (parser.yy.subQueries.length > 0) {
+        parser.yy.result.subQueries = parser.yy.subQueries;
+      }
+      if (
+        typeof parser.yy.result.suggestColumns.identifierChain === 'undefined' ||
+        parser.yy.result.suggestColumns.identifierChain.length === 0
+      ) {
+        if (tablePrimaries.length > 1) {
+          convertTablePrimariesToSuggestions(tablePrimaries);
+        } else {
+          suggestLateralViewAliasesAsIdentifiers();
+          if (
+            tablePrimaries.length === 1 &&
+            (tablePrimaries[0].alias || tablePrimaries[0].subQueryAlias)
+          ) {
+            convertTablePrimariesToSuggestions(tablePrimaries);
+          }
+          parser.expandIdentifierChain({
+            wrapper: parser.yy.result.suggestColumns,
+            anyOwner: false,
+            isColumnWrapper: true
+          });
+        }
+      } else if (!parser.yy.result.suggestColumns.linked) {
+        // Expand exploded views in the identifier chain
+        const originalLength = parser.yy.result.suggestColumns.identifierChain.length;
+        parser.yy.result.suggestColumns.identifierChain = parser.expandLateralViews(
+          parser.yy.lateralViews,
+          parser.yy.result.suggestColumns.identifierChain,
+          true
+        );
+        // Drop '*' keyword for lateral views
+        if (typeof parser.yy.result.suggestColumns !== 'undefined') {
+          if (
+            parser.yy.result.suggestColumns.identifierChain.length > originalLength &&
+            typeof parser.yy.result.suggestKeywords !== 'undefined' &&
+            parser.yy.result.suggestKeywords.length === 1 &&
+            parser.yy.result.suggestKeywords[0].value === '*'
+          ) {
+            delete parser.yy.result.suggestKeywords;
+          }
+          parser.expandIdentifierChain({
+            wrapper: parser.yy.result.suggestColumns,
+            anyOwner: false,
+            isColumnWrapper: true
+          });
+        }
+      } else {
+        parser.expandIdentifierChain({
+          wrapper: parser.yy.result.suggestColumns,
+          anyOwner: false,
+          isColumnWrapper: true
+        });
+      }
+    }
+
+    if (typeof parser.yy.result.colRef !== 'undefined' && !parser.yy.result.colRef.linked) {
+      parser.expandIdentifierChain({ wrapper: parser.yy.result.colRef });
+
+      const primaries = filterTablePrimariesForOwner(parser.yy.latestTablePrimaries);
+      if (
+        primaries.length === 0 ||
+        (primaries.length > 1 && parser.yy.result.colRef.identifierChain.length === 1)
+      ) {
+        parser.yy.result.colRef.identifierChain = [];
+      }
+    }
+    if (
+      typeof parser.yy.result.suggestKeyValues !== 'undefined' &&
+      !parser.yy.result.suggestKeyValues.linked
+    ) {
+      parser.expandIdentifierChain({ wrapper: parser.yy.result.suggestKeyValues });
+    }
+  };
+
+  parser.getSubQuery = function(cols) {
+    const columns = [];
+    cols.selectList.forEach(col => {
+      const result = {};
+      if (col.alias) {
+        result.alias = col.alias;
+      }
+      if (col.valueExpression && col.valueExpression.columnReference) {
+        result.identifierChain = col.valueExpression.columnReference;
+      } else if (col.asterisk) {
+        result.identifierChain = [{ asterisk: true }];
+      }
+      if (
+        col.valueExpression &&
+        col.valueExpression.types &&
+        col.valueExpression.types.length === 1
+      ) {
+        result.type = col.valueExpression.types[0];
+      }
+
+      columns.push(result);
+    });
+
+    return {
+      columns: columns
+    };
+  };
+
+  parser.addTablePrimary = function(ref) {
+    if (typeof parser.yy.latestTablePrimaries === 'undefined') {
+      parser.yy.latestTablePrimaries = [];
+    }
+    parser.yy.latestTablePrimaries.push(ref);
+  };
+
+  parser.suggestFileFormats = function() {
+    parser.suggestKeywords([
+      'AVRO',
+      'INPUTFORMAT',
+      'JSONFILE',
+      'ORC',
+      'PARQUET',
+      'RCFILE',
+      'SEQUENCEFILE',
+      'TEXTFILE'
+    ]);
+  };
+
+  parser.getKeywordsForOptionalsLR = function(optionals, keywords, override) {
+    let result = [];
+
+    for (let i = 0; i < optionals.length; i++) {
+      if (!optionals[i] && (typeof override === 'undefined' || override[i])) {
+        if (keywords[i] instanceof Array) {
+          result = result.concat(keywords[i]);
+        } else {
+          result.push(keywords[i]);
+        }
+      } else if (optionals[i]) {
+        break;
+      }
+    }
+    return result;
+  };
+
+  parser.suggestDdlAndDmlKeywords = function(extraKeywords) {
+    let keywords = [
+      'ALTER',
+      'CREATE',
+      'DESCRIBE',
+      'DROP',
+      'GRANT',
+      'INSERT',
+      'REVOKE',
+      'SELECT',
+      'SET',
+      'SHOW',
+      'TRUNCATE',
+      'UPDATE',
+      'USE',
+      'WITH'
+    ];
+
+    if (extraKeywords) {
+      keywords = keywords.concat(extraKeywords);
+    }
+
+    keywords = keywords.concat([
+      'ABORT',
+      'ANALYZE TABLE',
+      'DELETE',
+      'EXPORT',
+      'IMPORT',
+      'LOAD',
+      'MERGE',
+      'MSCK',
+      'RELOAD FUNCTION',
+      'RESET'
+    ]);
+
+    parser.suggestKeywords(keywords);
+  };
+
+  parser.checkForSelectListKeywords = function(selectList) {
+    if (selectList.length === 0) {
+      return;
+    }
+    const last = selectList[selectList.length - 1];
+    if (!last || !last.valueExpression) {
+      return;
+    }
+    const valueExpressionKeywords = parser.getValueExpressionKeywords(last.valueExpression);
+    let keywords = [];
+    if (last.suggestKeywords) {
+      keywords = keywords.concat(last.suggestKeywords);
+    }
+    if (valueExpressionKeywords.suggestKeywords) {
+      keywords = keywords.concat(valueExpressionKeywords.suggestKeywords);
+    }
+    if (valueExpressionKeywords.suggestColRefKeywords) {
+      parser.suggestColRefKeywords(valueExpressionKeywords.suggestColRefKeywords);
+      parser.addColRefIfExists(last.valueExpression);
+    }
+    if (!last.alias) {
+      keywords.push('AS');
+    }
+    if (keywords.length > 0) {
+      parser.suggestKeywords(keywords);
+    }
+  };
+
+  parser.checkForKeywords = function(expression) {
+    if (expression) {
+      if (expression.suggestKeywords && expression.suggestKeywords.length > 0) {
+        parser.suggestKeywords(expression.suggestKeywords);
+      }
+      if (expression.suggestColRefKeywords) {
+        parser.suggestColRefKeywords(expression.suggestColRefKeywords);
+        parser.addColRefIfExists(expression);
+      }
+    }
+  };
+
+  parser.createWeightedKeywords = function(keywords, weight) {
+    const result = [];
+    keywords.forEach(keyword => {
+      if (typeof keyword.weight !== 'undefined') {
+        keyword.weight = weight + keyword.weight / 10;
+        result.push(keyword);
+      } else {
+        result.push({ value: keyword, weight: weight });
+      }
+    });
+    return result;
+  };
+
+  parser.suggestKeywords = function(keywords) {
+    const weightedKeywords = [];
+    if (keywords.length === 0) {
+      return;
+    }
+    keywords.forEach(keyword => {
+      if (typeof keyword.weight !== 'undefined') {
+        weightedKeywords.push(keyword);
+      } else {
+        weightedKeywords.push({ value: keyword, weight: -1 });
+      }
+    });
+    weightedKeywords.sort((a, b) => {
+      if (a.weight !== b.weight) {
+        return b.weight - a.weight;
+      }
+      return a.value.localeCompare(b.value);
+    });
+    parser.yy.result.suggestKeywords = weightedKeywords;
+  };
+
+  parser.suggestColRefKeywords = function(colRefKeywords) {
+    parser.yy.result.suggestColRefKeywords = colRefKeywords;
+  };
+
+  parser.suggestTablesOrColumns = function(identifier) {
+    if (typeof parser.yy.latestTablePrimaries == 'undefined') {
+      parser.suggestTables({ identifierChain: [{ name: identifier }] });
+      return;
+    }
+    const tableRef = parser.yy.latestTablePrimaries.filter(tablePrimary => {
+      return equalIgnoreCase(tablePrimary.alias, identifier);
+    });
+    if (tableRef.length > 0) {
+      parser.suggestColumns({ identifierChain: [{ name: identifier }] });
+    } else {
+      parser.suggestTables({ identifierChain: [{ name: identifier }] });
+    }
+  };
+
+  parser.suggestFunctions = function(details) {
+    parser.yy.result.suggestFunctions = details || {};
+  };
+
+  parser.suggestAggregateFunctions = function() {
+    const primaries = [];
+    const aliases = {};
+    parser.yy.latestTablePrimaries.forEach(primary => {
+      if (typeof primary.alias !== 'undefined') {
+        aliases[primary.alias] = true;
+      }
+      // Drop if the first one refers to a table alias (...FROM tbl t, t.map tm ...)
+      if (
+        typeof primary.identifierChain !== 'undefined' &&
+        !aliases[primary.identifierChain[0].name] &&
+        typeof primary.owner === 'undefined'
+      ) {
+        primaries.push(primary);
+      }
+    });
+    parser.yy.result.suggestAggregateFunctions = { tablePrimaries: primaries };
+  };
+
+  parser.suggestAnalyticFunctions = function() {
+    parser.yy.result.suggestAnalyticFunctions = true;
+  };
+
+  parser.suggestSetOptions = function() {
+    parser.yy.result.suggestSetOptions = true;
+  };
+
+  parser.suggestIdentifiers = function(identifiers) {
+    parser.yy.result.suggestIdentifiers = identifiers;
+  };
+
+  parser.suggestColumns = function(details) {
+    if (typeof details === 'undefined') {
+      details = { identifierChain: [] };
+    } else if (typeof details.identifierChain === 'undefined') {
+      details.identifierChain = [];
+    }
+    parser.yy.result.suggestColumns = details;
+  };
+
+  parser.suggestGroupBys = function(details) {
+    parser.yy.result.suggestGroupBys = details || {};
+  };
+
+  parser.suggestOrderBys = function(details) {
+    parser.yy.result.suggestOrderBys = details || {};
+  };
+
+  parser.suggestFilters = function(details) {
+    parser.yy.result.suggestFilters = details || {};
+  };
+
+  parser.suggestKeyValues = function(details) {
+    parser.yy.result.suggestKeyValues = details || {};
+  };
+
+  parser.suggestTables = function(details) {
+    parser.yy.result.suggestTables = details || {};
+  };
+
+  const adjustLocationForCursor = function(location) {
+    // columns are 0-based and lines not, so add 1 to cols
+    const newLocation = {
+      first_line: location.first_line,
+      last_line: location.last_line,
+      first_column: location.first_column + 1,
+      last_column: location.last_column + 1
+    };
+    if (parser.yy.cursorFound) {
+      if (
+        parser.yy.cursorFound.first_line === newLocation.first_line &&
+        parser.yy.cursorFound.last_column <= newLocation.first_column
+      ) {
+        let additionalSpace = parser.yy.partialLengths.left + parser.yy.partialLengths.right;
+        additionalSpace -= parser.yy.partialCursor ? 1 : 3; // For some reason the normal cursor eats 3 positions.
+        newLocation.first_column = newLocation.first_column + additionalSpace;
+        newLocation.last_column = newLocation.last_column + additionalSpace;
+      }
+    }
+    return newLocation;
+  };
+
+  parser.addFunctionLocation = function(location, functionName) {
+    // Remove trailing '(' from location
+    const adjustedLocation = {
+      first_line: location.first_line,
+      last_line: location.last_line,
+      first_column: location.first_column,
+      last_column: location.last_column - 1
+    };
+    parser.yy.locations.push({
+      type: 'function',
+      location: adjustLocationForCursor(adjustedLocation),
+      function: functionName.toLowerCase()
+    });
+  };
+
+  parser.addStatementLocation = function(location) {
+    // Don't report lonely cursor as a statement
+    if (
+      location.first_line === location.last_line &&
+      Math.abs(location.last_column - location.first_column) === 1
+    ) {
+      return;
+    }
+    let adjustedLocation;
+    if (
+      parser.yy.cursorFound &&
+      parser.yy.cursorFound.last_line === location.last_line &&
+      parser.yy.cursorFound.first_column >= location.first_column &&
+      parser.yy.cursorFound.last_column <= location.last_column
+    ) {
+      const additionalSpace = parser.yy.partialLengths.left + parser.yy.partialLengths.right;
+      adjustedLocation = {
+        first_line: location.first_line,
+        last_line: location.last_line,
+        first_column: location.first_column + 1,
+        last_column: location.last_column + additionalSpace - (parser.yy.partialCursor ? 0 : 2)
+      };
+    } else {
+      adjustedLocation = {
+        first_line: location.first_line,
+        last_line: location.last_line,
+        first_column: location.first_column + 1,
+        last_column: location.last_column + 1
+      };
+    }
+
+    parser.yy.locations.push({
+      type: 'statement',
+      location: adjustedLocation
+    });
+  };
+
+  parser.firstDefined = function() {
+    for (let i = 0; i + 1 < arguments.length; i += 2) {
+      if (arguments[i]) {
+        return arguments[i + 1];
+      }
+    }
+  };
+
+  parser.addClauseLocation = function(type, precedingLocation, locationIfPresent, isCursor) {
+    let location;
+    if (isCursor) {
+      if (parser.yy.partialLengths.left === 0 && parser.yy.partialLengths.right === 0) {
+        location = {
+          type: type,
+          missing: true,
+          location: adjustLocationForCursor({
+            first_line: precedingLocation.last_line,
+            first_column: precedingLocation.last_column,
+            last_line: precedingLocation.last_line,
+            last_column: precedingLocation.last_column
+          })
+        };
+      } else {
+        location = {
+          type: type,
+          missing: false,
+          location: {
+            first_line: locationIfPresent.last_line,
+            first_column: locationIfPresent.last_column - 1,
+            last_line: locationIfPresent.last_line,
+            last_column:
+              locationIfPresent.last_column -
+              1 +
+              parser.yy.partialLengths.right +
+              parser.yy.partialLengths.left
+          }
+        };
+      }
+    } else {
+      location = {
+        type: type,
+        missing: !locationIfPresent,
+        location: adjustLocationForCursor(
+          locationIfPresent || {
+            first_line: precedingLocation.last_line,
+            first_column: precedingLocation.last_column,
+            last_line: precedingLocation.last_line,
+            last_column: precedingLocation.last_column
+          }
+        )
+      };
+    }
+    if (parser.isInSubquery()) {
+      location.subquery = true;
+    }
+    parser.yy.locations.push(location);
+  };
+
+  parser.addStatementTypeLocation = function(identifier, location, additionalText) {
+    // Don't add if already there except for SELECT
+    if (identifier !== 'SELECT' && parser.yy.allLocations) {
+      for (let i = parser.yy.allLocations.length - 1; i >= 0; i--) {
+        if (parser.yy.allLocations[i] && parser.yy.allLocations[i].type === 'statement') {
+          break;
+        }
+        if (parser.yy.allLocations[i] && parser.yy.allLocations[i].type === 'statementType') {
+          return;
+        }
+      }
+    }
+    const loc = {
+      type: 'statementType',
+      location: adjustLocationForCursor(location),
+      identifier: identifier
+    };
+    if (typeof additionalText !== 'undefined') {
+      switch (identifier) {
+        case 'ALTER':
+          if (/ALTER\s+VIEW/i.test(additionalText)) {
+            loc.identifier = 'ALTER VIEW';
+          } else {
+            loc.identifier = 'ALTER TABLE';
+          }
+          break;
+        case 'COMPUTE':
+          loc.identifier = 'COMPUTE STATS';
+          break;
+        case 'CREATE':
+          if (/CREATE\s+VIEW/i.test(additionalText)) {
+            loc.identifier = 'CREATE VIEW';
+          } else if (/CREATE\s+TABLE/i.test(additionalText)) {
+            loc.identifier = 'CREATE TABLE';
+          } else if (/CREATE\s+DATABASE/i.test(additionalText)) {
+            loc.identifier = 'CREATE DATABASE';
+          } else if (/CREATE\s+ROLE/i.test(additionalText)) {
+            loc.identifier = 'CREATE ROLE';
+          } else if (/CREATE\s+FUNCTION/i.test(additionalText)) {
+            loc.identifier = 'CREATE FUNCTION';
+          } else {
+            loc.identifier = 'CREATE TABLE';
+          }
+          break;
+        case 'DROP':
+          if (/DROP\s+VIEW/i.test(additionalText)) {
+            loc.identifier = 'DROP VIEW';
+          } else if (/DROP\s+TABLE/i.test(additionalText)) {
+            loc.identifier = 'DROP TABLE';
+          } else if (/DROP\s+DATABASE/i.test(additionalText)) {
+            loc.identifier = 'DROP DATABASE';
+          } else if (/DROP\s+ROLE/i.test(additionalText)) {
+            loc.identifier = 'DROP ROLE';
+          } else if (/DROP\s+STATS/i.test(additionalText)) {
+            loc.identifier = 'DROP STATS';
+          } else if (/DROP\s+FUNCTION/i.test(additionalText)) {
+            loc.identifier = 'DROP FUNCTION';
+          } else {
+            loc.identifier = 'DROP TABLE';
+          }
+          break;
+        case 'INVALIDATE':
+          loc.identifier = 'INVALIDATE METADATA';
+          break;
+        case 'LOAD':
+          loc.identifier = 'LOAD DATA';
+          break;
+        case 'TRUNCATE':
+          loc.identifier = 'TRUNCATE TABLE';
+          break;
+        default:
+      }
+    }
+    parser.yy.locations.push(loc);
+  };
+
+  parser.addFileLocation = function(location, path) {
+    parser.yy.locations.push({
+      type: 'file',
+      location: adjustLocationForCursor(location),
+      path: path
+    });
+  };
+
+  parser.addDatabaseLocation = function(location, identifierChain) {
+    parser.yy.locations.push({
+      type: 'database',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addTableLocation = function(location, identifierChain) {
+    parser.yy.locations.push({
+      type: 'table',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addColumnAliasLocation = function(location, alias, parentLocation) {
+    const aliasLocation = {
+      type: 'alias',
+      source: 'column',
+      alias: alias,
+      location: adjustLocationForCursor(location),
+      parentLocation: adjustLocationForCursor(parentLocation)
+    };
+    if (
+      parser.yy.locations.length &&
+      parser.yy.locations[parser.yy.locations.length - 1].type === 'column'
+    ) {
+      const closestColumn = parser.yy.locations[parser.yy.locations.length - 1];
+      if (
+        closestColumn.location.first_line === aliasLocation.parentLocation.first_line &&
+        closestColumn.location.last_line === aliasLocation.parentLocation.last_line &&
+        closestColumn.location.first_column === aliasLocation.parentLocation.first_column &&
+        closestColumn.location.last_column === aliasLocation.parentLocation.last_column
+      ) {
+        parser.yy.locations[parser.yy.locations.length - 1].alias = alias;
+      }
+    }
+    parser.yy.locations.push(aliasLocation);
+  };
+
+  parser.addTableAliasLocation = function(location, alias, identifierChain) {
+    parser.yy.locations.push({
+      type: 'alias',
+      source: 'table',
+      alias: alias,
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addSubqueryAliasLocation = function(location, alias) {
+    parser.yy.locations.push({
+      type: 'alias',
+      source: 'subquery',
+      alias: alias,
+      location: adjustLocationForCursor(location)
+    });
+  };
+
+  parser.addAsteriskLocation = function(location, identifierChain) {
+    parser.yy.locations.push({
+      type: 'asterisk',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addVariableLocation = function(location, value) {
+    if (/\${[^}]*}/.test(value)) {
+      parser.yy.locations.push({
+        type: 'variable',
+        location: adjustLocationForCursor(location),
+        value: value
+      });
+    }
+  };
+
+  parser.addColumnLocation = function(location, identifierChain) {
+    const isVariable =
+      identifierChain.length && /\${[^}]*}/.test(identifierChain[identifierChain.length - 1].name);
+    if (isVariable) {
+      parser.yy.locations.push({
+        type: 'variable',
+        location: adjustLocationForCursor(location),
+        value: identifierChain[identifierChain.length - 1].name
+      });
+    } else {
+      parser.yy.locations.push({
+        type: 'column',
+        location: adjustLocationForCursor(location),
+        identifierChain: identifierChain,
+        qualified: identifierChain.length > 1
+      });
+    }
+  };
+
+  parser.addCteAliasLocation = function(location, alias) {
+    parser.yy.locations.push({
+      type: 'alias',
+      source: 'cte',
+      alias: alias,
+      location: adjustLocationForCursor(location)
+    });
+  };
+
+  parser.addUnknownLocation = function(location, identifierChain) {
+    const isVariable =
+      identifierChain.length && /\${[^}]*}/.test(identifierChain[identifierChain.length - 1].name);
+    let loc;
+    if (isVariable) {
+      loc = {
+        type: 'variable',
+        location: adjustLocationForCursor(location),
+        value: identifierChain[identifierChain.length - 1].name
+      };
+    } else {
+      loc = {
+        type: 'unknown',
+        location: adjustLocationForCursor(location),
+        identifierChain: identifierChain,
+        qualified: identifierChain.length > 1
+      };
+    }
+    parser.yy.locations.push(loc);
+    return loc;
+  };
+
+  parser.addNewDatabaseLocation = function(location, identifierChain) {
+    parser.yy.definitions.push({
+      type: 'database',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addNewTableLocation = function(location, identifierChain, colSpec) {
+    const columns = [];
+    if (colSpec) {
+      colSpec.forEach(col => {
+        columns.push({
+          identifierChain: [col.identifier], // TODO: Complex
+          type: col.type,
+          location: adjustLocationForCursor(col.location)
+        });
+      });
+    }
+    parser.yy.definitions.push({
+      type: 'table',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain,
+      columns: columns
+    });
+  };
+
+  parser.addColRefToVariableIfExists = function(left, right) {
+    if (
+      left &&
+      left.columnReference &&
+      left.columnReference.length &&
+      right &&
+      right.columnReference &&
+      right.columnReference.length &&
+      parser.yy.locations.length > 1
+    ) {
+      const addColRefToVariableLocation = function(variableValue, colRef) {
+        // See if colref is actually an alias
+        if (colRef.length === 1 && colRef[0].name) {
+          parser.yy.locations.some(location => {
+            if (location.type === 'column' && location.alias === colRef[0].name) {
+              colRef = location.identifierChain;
+              return true;
+            }
+          });
+        }
+
+        for (let i = parser.yy.locations.length - 1; i > 0; i--) {
+          const location = parser.yy.locations[i];
+          if (location.type === 'variable' && location.value === variableValue) {
+            location.colRef = { identifierChain: colRef };
+            break;
+          }
+        }
+      };
+
+      if (/\${[^}]*}/.test(left.columnReference[0].name)) {
+        // left is variable
+        addColRefToVariableLocation(left.columnReference[0].name, right.columnReference);
+      } else if (/\${[^}]*}/.test(right.columnReference[0].name)) {
+        // right is variable
+        addColRefToVariableLocation(right.columnReference[0].name, left.columnReference);
+      }
+    }
+  };
+
+  parser.suggestDatabases = function(details) {
+    parser.yy.result.suggestDatabases = details || {};
+  };
+
+  parser.suggestHdfs = function(details) {
+    parser.yy.result.suggestHdfs = details || {};
+  };
+
+  parser.suggestValues = function(details) {
+    parser.yy.result.suggestValues = details || {};
+  };
+
+  parser.determineCase = function(text) {
+    if (!parser.yy.caseDetermined) {
+      parser.yy.lowerCase = text.toLowerCase() === text;
+      parser.yy.caseDetermined = true;
+    }
+  };
+
+  parser.handleQuotedValueWithCursor = function(lexer, yytext, yylloc, quoteChar) {
+    if (yytext.indexOf('\u2020') !== -1 || yytext.indexOf('\u2021') !== -1) {
+      parser.yy.partialCursor = yytext.indexOf('\u2021') !== -1;
+      const cursorIndex = parser.yy.partialCursor
+        ? yytext.indexOf('\u2021')
+        : yytext.indexOf('\u2020');
+      parser.yy.cursorFound = {
+        first_line: yylloc.first_line,
+        last_line: yylloc.last_line,
+        first_column: yylloc.first_column + cursorIndex,
+        last_column: yylloc.first_column + cursorIndex + 1
+      };
+      const remainder = yytext.substring(cursorIndex + 1);
+      const remainingQuotes = (lexer.upcomingInput().match(new RegExp(quoteChar, 'g')) || [])
+        .length;
+      if (remainingQuotes > 0 && (remainingQuotes & 1) !== 0) {
+        parser.yy.missingEndQuote = false;
+        lexer.input();
+      } else {
+        parser.yy.missingEndQuote = true;
+        lexer.unput(remainder);
+      }
+      lexer.popState();
+      return true;
+    }
+    return false;
+  };
+
+  let lexerModified = false;
+
+  /**
+   * Main parser function
+   */
+  parser.parseSql = function(beforeCursor, afterCursor, debug) {
+    // Jison counts CRLF as two lines in the locations
+    beforeCursor = beforeCursor.replace(/\r\n|\n\r/gm, '\n');
+    afterCursor = afterCursor.replace(/\r\n|\n\r/gm, '\n');
+    parser.yy.result = { locations: [] };
+    parser.yy.lowerCase = false;
+    parser.yy.locations = [];
+    parser.yy.definitions = [];
+    parser.yy.allLocations = [];
+    parser.yy.subQueries = [];
+    parser.yy.errors = [];
+    parser.yy.selectListAliases = [];
+
+    parser.yy.locationsStack = [];
+    parser.yy.primariesStack = [];
+    parser.yy.lateralViewsStack = [];
+    parser.yy.subQueriesStack = [];
+    parser.yy.resultStack = [];
+    parser.yy.selectListAliasesStack = [];
+    parser.yy.activeDialect = 'hive';
+
+    delete parser.yy.caseDetermined;
+    delete parser.yy.cursorFound;
+    delete parser.yy.partialCursor;
+
+    parser.prepareNewStatement();
+
+    const REASONABLE_SURROUNDING_LENGTH = 150000; // About 3000 lines before and after
+
+    if (beforeCursor.length > REASONABLE_SURROUNDING_LENGTH) {
+      if (beforeCursor.length - beforeCursor.lastIndexOf(';') > REASONABLE_SURROUNDING_LENGTH) {
+        // Bail out if the last complete statement is more than 150000 chars before
+        return {};
+      }
+      // Cut it at the first statement found within 150000 chars before
+      const lastReasonableChunk = beforeCursor.substring(
+        beforeCursor.length - REASONABLE_SURROUNDING_LENGTH
+      );
+      beforeCursor = lastReasonableChunk.substring(lastReasonableChunk.indexOf(';') + 1);
+    }
+
+    if (afterCursor.length > REASONABLE_SURROUNDING_LENGTH) {
+      if (afterCursor.length - afterCursor.indexOf(';') > REASONABLE_SURROUNDING_LENGTH) {
+        // No need to bail out for what's comes after, we can still get keyword completion
+        afterCursor = '';
+      } else {
+        // Cut it at the last statement found within 150000 chars after
+        const firstReasonableChunk = afterCursor.substring(0, REASONABLE_SURROUNDING_LENGTH);
+        afterCursor = firstReasonableChunk.substring(0, firstReasonableChunk.lastIndexOf(';'));
+      }
+    }
+
+    parser.yy.partialLengths = parser.identifyPartials(beforeCursor, afterCursor);
+
+    if (parser.yy.partialLengths.left > 0) {
+      beforeCursor = beforeCursor.substring(0, beforeCursor.length - parser.yy.partialLengths.left);
+    }
+
+    if (parser.yy.partialLengths.right > 0) {
+      afterCursor = afterCursor.substring(parser.yy.partialLengths.right);
+    }
+
+    let result;
+    try {
+      // Add |CURSOR| or |PARTIAL_CURSOR| to represent the different cursor states in the lexer
+      result = parser.parse(
+        beforeCursor +
+          (beforeCursor.length === 0 || /[\s(]$/.test(beforeCursor) ? ' \u2020 ' : '\u2021') +
+          afterCursor
+      );
+    } catch (err) {
+      // On any error try to at least return any existing result
+      if (typeof parser.yy.result === 'undefined') {
+        throw err;
+      }
+      if (debug) {
+        console.log(err);
+        console.error(err.stack);
+      }
+      result = parser.yy.result;
+    }
+    if (parser.yy.errors.length > 0) {
+      parser.yy.result.errors = parser.yy.errors;
+      if (debug) {
+        console.log(parser.yy.errors);
+      }
+    }
+    try {
+      linkTablePrimaries();
+      parser.commitLocations();
+      // Clean up and prioritize
+      prioritizeSuggestions();
+    } catch (err) {
+      if (debug) {
+        console.log(err);
+        console.error(err.stack);
+      }
+    }
+
+    parser.yy.allLocations.sort((a, b) => {
+      if (a.location.first_line !== b.location.first_line) {
+        return a.location.first_line - b.location.first_line;
+      }
+      if (a.location.first_column !== b.location.first_column) {
+        return a.location.first_column - b.location.first_column;
+      }
+      if (a.location.last_column !== b.location.last_column) {
+        return b.location.last_column - a.location.last_column;
+      }
+      return b.type.localeCompare(a.type);
+    });
+    parser.yy.result.locations = parser.yy.allLocations;
+    parser.yy.result.definitions = parser.yy.definitions;
+
+    parser.yy.result.locations.forEach(location => {
+      delete location.linked;
+    });
+    if (typeof parser.yy.result.suggestColumns !== 'undefined') {
+      delete parser.yy.result.suggestColumns.linked;
+    }
+
+    SIMPLE_TABLE_REF_SUGGESTIONS.forEach(suggestionType => {
+      if (typeof parser.yy.result[suggestionType] !== 'undefined') {
+        delete parser.yy.result[suggestionType].linked;
+      }
+    });
+
+    if (typeof parser.yy.result.colRef !== 'undefined') {
+      delete parser.yy.result.colRef.linked;
+    }
+    if (typeof parser.yy.result.suggestKeyValues !== 'undefined') {
+      delete parser.yy.result.suggestKeyValues.linked;
+    }
+
+    if (typeof result.error !== 'undefined' && typeof result.error.expected !== 'undefined') {
+      // Remove the cursor from expected tokens
+      result.error.expected = result.error.expected.filter(token => token.indexOf('CURSOR') === -1)
+    }
+
+    if (typeof result.error !== 'undefined' && result.error.recoverable) {
+      delete result.error;
+    }
+
+    // Adjust all the statement locations to include white space surrounding them
+    let lastStatementLocation = null;
+    result.locations.forEach(location => {
+      if (location.type === 'statement') {
+        if (lastStatementLocation === null) {
+          location.location.first_line = 1;
+          location.location.first_column = 1;
+        } else {
+          location.location.first_line = lastStatementLocation.location.last_line;
+          location.location.first_column = lastStatementLocation.location.last_column + 1;
+        }
+        lastStatementLocation = location;
+      }
+    });
+
+    return result;
+  };
+};
+
+const SYNTAX_PARSER_NOOP_FUNCTIONS = [
+  'addAsteriskLocation',
+  'addClauseLocation',
+  'addColRefIfExists',
+  'addColRefToVariableIfExists',
+  'addColumnAliasLocation',
+  'addColumnLocation',
+  'addCommonTableExpressions',
+  'addCteAliasLocation',
+  'addDatabaseLocation',
+  'addFileLocation',
+  'addFunctionLocation',
+  'addNewDatabaseLocation',
+  'addNewTableLocation',
+  'addStatementLocation',
+  'addStatementTypeLocation',
+  'addSubqueryAliasLocation',
+  'addTableAliasLocation',
+  'addTableLocation',
+  'addTablePrimary',
+  'addUnknownLocation',
+  'addVariableLocation',
+  'applyArgumentTypesToSuggestions',
+  'applyTypeToSuggestions',
+  'checkForKeywords',
+  'checkForSelectListKeywords',
+  'commitLocations',
+  'firstDefined',
+  'getSelectListKeywords',
+  'getSubQuery',
+  'getValueExpressionKeywords',
+  'identifyPartials',
+  'popQueryState',
+  'prepareNewStatement',
+  'pushQueryState',
+  'selectListNoTableSuggest',
+  'suggestAggregateFunctions',
+  'suggestAnalyticFunctions',
+  'suggestColRefKeywords',
+  'suggestColumns',
+  'suggestDatabases',
+  'suggestDdlAndDmlKeywords',
+  'suggestFileFormats',
+  'suggestFilters',
+  'suggestFunctions',
+  'suggestGroupBys',
+  'suggestHdfs',
+  'suggestIdentifiers',
+  'suggestJoinConditions',
+  'suggestJoins',
+  'suggestKeyValues',
+  'suggestKeywords',
+  'suggestOrderBys',
+  'suggestSelectListAliases',
+  'suggestTables',
+  'suggestTablesOrColumns',
+  'suggestValueExpressionKeywords',
+  'suggestValues',
+  'valueExpressionSuggest'
+];
+
+const SYNTAX_PARSER_NOOP = function() {};
+
+const initSyntaxParser = function(parser) {
+  // Noop functions for compatibility with the autocomplete parser as the grammar is shared
+  SYNTAX_PARSER_NOOP_FUNCTIONS.forEach(noopFn => {
+    parser[noopFn] = SYNTAX_PARSER_NOOP;
+  });
+
+  parser.yy.locations = [{}];
+
+  parser.determineCase = function(text) {
+    if (!parser.yy.caseDetermined) {
+      parser.yy.lowerCase = text.toLowerCase() === text;
+      parser.yy.caseDetermined = true;
+    }
+  };
+
+  parser.getKeywordsForOptionalsLR = function() {
+    return [];
+  };
+
+  parser.mergeSuggestKeywords = function() {
+    return {};
+  };
+
+  parser.getTypeKeywords = function() {
+    return [];
+  };
+
+  parser.getColumnDataTypeKeywords = function() {
+    return [];
+  };
+
+  parser.findCaseType = function() {
+    return { types: ['T'] };
+  };
+
+  parser.findReturnTypes = function() {
+    return ['T'];
+  };
+
+  parser.expandIdentifierChain = function() {
+    return [];
+  };
+
+  parser.expandLateralViews = function() {
+    return [];
+  };
+
+  parser.createWeightedKeywords = function() {
+    return [];
+  };
+
+  parser.handleQuotedValueWithCursor = function(lexer, yytext, yylloc, quoteChar) {
+    if (yytext.indexOf('\u2020') !== -1 || yytext.indexOf('\u2021') !== -1) {
+      parser.yy.partialCursor = yytext.indexOf('\u2021') !== -1;
+      const cursorIndex = parser.yy.partialCursor
+        ? yytext.indexOf('\u2021')
+        : yytext.indexOf('\u2020');
+      parser.yy.cursorFound = {
+        first_line: yylloc.first_line,
+        last_line: yylloc.last_line,
+        first_column: yylloc.first_column + cursorIndex,
+        last_column: yylloc.first_column + cursorIndex + 1
+      };
+      const remainder = yytext.substring(cursorIndex + 1);
+      const remainingQuotes = (lexer.upcomingInput().match(new RegExp(quoteChar, 'g')) || [])
+        .length;
+      if (remainingQuotes > 0 && (remainingQuotes & 1) !== 0) {
+        parser.yy.missingEndQuote = false;
+        lexer.input();
+      } else {
+        parser.yy.missingEndQuote = true;
+        lexer.unput(remainder);
+      }
+      lexer.popState();
+      return true;
+    }
+    return false;
+  };
+
+  let lexerModified = false;
+
+  parser.yy.parseError = function(str, hash) {
+    parser.yy.error = hash;
+  };
+
+  const IGNORED_EXPECTED = {
+    ';': true,
+    '.': true,
+    EOF: true,
+    UNSIGNED_INTEGER: true,
+    UNSIGNED_INTEGER_E: true,
+    REGULAR_IDENTIFIER: true,
+    CURSOR: true,
+    PARTIAL_CURSOR: true,
+    HDFS_START_QUOTE: true,
+    HDFS_PATH: true,
+    HDFS_END_QUOTE: true,
+    COMPARISON_OPERATOR: true, // TODO: Expand in results when found
+    ARITHMETIC_OPERATOR: true, // TODO: Expand in results when found
+    VARIABLE_REFERENCE: true,
+    BACKTICK: true,
+    VALUE: true,
+    PARTIAL_VALUE: true,
+    SINGLE_QUOTE: true,
+    DOUBLE_QUOTE: true
+  };
+
+  const CLEAN_EXPECTED = {
+    BETWEEN_AND: 'AND',
+    OVERWRITE_DIRECTORY: 'OVERWRITE',
+    STORED_AS_DIRECTORIES: 'STORED',
+    LIKE_PARQUET: 'LIKE',
+    PARTITION_VALUE: 'PARTITION'
+  };
+
+  parser.parseSyntax = function(beforeCursor, afterCursor, debug) {
+    parser.yy.caseDetermined = false;
+    parser.yy.error = undefined;
+
+    parser.yy.latestTablePrimaries = [];
+    parser.yy.subQueries = [];
+    parser.yy.selectListAliases = [];
+    parser.yy.latestTablePrimaries = [];
+
+    parser.yy.activeDialect = 'hive';
+
+    // TODO: Find a way around throwing an exception when the parser finds a syntax error
+    try {
+      parser.yy.error = false;
+      parser.parse(beforeCursor + afterCursor);
+    } catch (err) {
+      if (debug) {
+        console.log(err);
+        console.error(err.stack);
+        console.log(parser.yy.error);
+      }
+    }
+
+    if (
+      parser.yy.error &&
+      (parser.yy.error.loc.last_column < beforeCursor.length ||
+        !beforeCursor.endsWith(parser.yy.error.text))
+    ) {
+      const weightedExpected = [];
+
+      const addedExpected = {};
+
+      const isLowerCase =
+        (parser.yy.caseDetermined && parser.yy.lowerCase) ||
+        parser.yy.error.text.toLowerCase() === parser.yy.error.text;
+
+      if (
+        parser.yy.error.expected.length === 2 &&
+        parser.yy.error.expected.indexOf("';'") !== -1 &&
+        parser.yy.error.expected.indexOf("'EOF'") !== -1
+      ) {
+        parser.yy.error.expected = [];
+        parser.yy.error.expectedStatementEnd = true;
+        return parser.yy.error;
+      }
+      for (let i = 0; i < parser.yy.error.expected.length; i++) {
+        let expected = parser.yy.error.expected[i];
+        // Strip away the surrounding ' chars
+        expected = expected.substring(1, expected.length - 1);
+        // TODO: Only suggest alphanumeric?
+        if (expected === 'REGULAR_IDENTIFIER') {
+          parser.yy.error.expectedIdentifier = true;
+          if (/^<[a-z]+>/.test(parser.yy.error.token)) {
+            const text = '`' + parser.yy.error.text + '`';
+            weightedExpected.push({
+              text: text,
+              distance: stringDistance(parser.yy.error.text, text, true)
+            });
+            parser.yy.error.possibleReserved = true;
+          }
+        } else if (!IGNORED_EXPECTED[expected] && /[a-z_]+/i.test(expected)) {
+          if (/^<[a-z]+>/.test(expected)) {
+            continue;
+          }
+          expected = CLEAN_EXPECTED[expected] || expected;
+          if (expected === parser.yy.error.text.toUpperCase()) {
+            // Can happen when the lexer entry for a rule contains multiple words like 'stored' in 'stored as parquet'
+            return false;
+          }
+          const text = isLowerCase ? expected.toLowerCase() : expected;
+          if (text && !addedExpected[text]) {
+            addedExpected[text] = true;
+            weightedExpected.push({
+              text: text,
+              distance: stringDistance(parser.yy.error.text, text, true)
+            });
+          }
+        }
+      }
+      if (weightedExpected.length === 0) {
+        parser.yy.error.expected = [];
+        parser.yy.error.incompleteStatement = true;
+        return parser.yy.error;
+      }
+      weightedExpected.sort((a, b) => {
+        if (a.distance === b.distance) {
+          return a.text.localeCompare(b.text);
+        }
+        return a.distance - b.distance;
+      });
+      parser.yy.error.expected = weightedExpected;
+      parser.yy.error.incompleteStatement = true;
+      return parser.yy.error;
+    } else if (parser.yy.error) {
+      parser.yy.error.expected = [];
+      parser.yy.error.incompleteStatement = true;
+      return parser.yy.error;
+    }
+    return false;
+  };
+};
+
+const initGlobalSearchParser = function(parser) {
+  parser.identifyPartials = function(beforeCursor, afterCursor) {
+    const beforeMatch = beforeCursor.match(/[0-9a-zA-Z_]*$/);
+    const afterMatch = afterCursor.match(/^[0-9a-zA-Z_]*(?:\((?:[^)]*\))?)?/);
+    return {
+      left: beforeMatch ? beforeMatch[0].length : 0,
+      right: afterMatch ? afterMatch[0].length : 0
+    };
+  };
+
+  parser.mergeFacets = function(a, b) {
+    if (!a.facets) {
+      a.facets = {};
+    }
+    if (!b.facets) {
+      return;
+    }
+    Object.keys(b.facets).forEach(key => {
+      if (a.facets[key]) {
+        Object.keys(b.facets[key]).forEach(val => {
+          a.facets[key][val.toLowerCase()] = true;
+        });
+      } else {
+        a.facets[key] = b.facets[key];
+      }
+    });
+  };
+
+  parser.mergeText = function(a, b) {
+    if (!a.text) {
+      a.text = [];
+    }
+    if (!b.text) {
+      return;
+    }
+    a.text = a.text.concat(b.text);
+  };
+
+  parser.handleQuotedValueWithCursor = function(lexer, yytext, yylloc, quoteChar) {
+    if (yytext.indexOf('\u2020') !== -1 || yytext.indexOf('\u2021') !== -1) {
+      const cursorIndex = yytext.indexOf('\u2020');
+      parser.yy.cursorFound = {
+        first_line: yylloc.first_line,
+        last_line: yylloc.last_line,
+        first_column: yylloc.first_column + cursorIndex,
+        last_column: yylloc.first_column + cursorIndex + 1
+      };
+      const remainder = yytext.substring(cursorIndex + 1);
+      const remainingQuotes = (lexer.upcomingInput().match(new RegExp(quoteChar, 'g')) || [])
+        .length;
+      if (remainingQuotes > 0 && (remainingQuotes & 1) !== 0) {
+        parser.yy.missingEndQuote = false;
+        lexer.input();
+      } else {
+        parser.yy.missingEndQuote = true;
+        lexer.unput(remainder);
+      }
+      lexer.popState();
+      return true;
+    }
+    return false;
+  };
+
+  parser.parseGlobalSearch = function(beforeCursor, afterCursor, debug) {
+    delete parser.yy.cursorFound;
+
+    let result;
+    try {
+      result = parser.parse(beforeCursor + '\u2020' + afterCursor);
+    } catch (err) {
+      if (debug) {
+        console.log(err);
+        console.error(err.stack);
+        console.log(parser.yy.error);
+      }
+      return {
+        facets: {},
+        text: []
+      };
+    }
+    return result;
+  };
+};
+
+export default {
+  initSqlParser: initSqlParser,
+  initSyntaxParser: initSyntaxParser,
+  stringDistance: stringDistance,
+  initGlobalSearchParser: initGlobalSearchParser
+};

Разница между файлами не показана из-за своего большого размера
+ 91 - 0
desktop/core/src/desktop/js/parse/sql/impala/impalaAutocompleteParser.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
desktop/core/src/desktop/js/parse/sql/impala/impalaSyntaxParser.js


+ 2590 - 0
desktop/core/src/desktop/js/parse/sql/impala/sqlParseSupport.js

@@ -0,0 +1,2590 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 { SqlFunctions } from 'sql/sqlFunctions';
+
+const identifierEquals = (a, b) =>
+  a &&
+  b &&
+  a
+    .replace(/^\s*`/, '')
+    .replace(/`\s*$/, '')
+    .toLowerCase() ===
+    b
+      .replace(/^\s*`/, '')
+      .replace(/`\s*$/, '')
+      .toLowerCase();
+
+// endsWith polyfill from hue_utils.js, needed as workers live in their own js environment
+if (!String.prototype.endsWith) {
+  String.prototype.endsWith = function(searchString, position) {
+    const subjectString = this.toString();
+    if (
+      typeof position !== 'number' ||
+      !isFinite(position) ||
+      Math.floor(position) !== position ||
+      position > subjectString.length
+    ) {
+      position = subjectString.length;
+    }
+    position -= searchString.length;
+    const lastIndex = subjectString.lastIndexOf(searchString, position);
+    return lastIndex !== -1 && lastIndex === position;
+  };
+}
+
+/**
+ * Calculates the Optimal String Alignment distance between two strings. Returns 0 when the strings are equal and the
+ * distance when not, distances is less than or equal to the length of the longest string.
+ *
+ * @param strA
+ * @param strB
+ * @param [ignoreCase]
+ * @returns {number} The similarity
+ */
+const stringDistance = function(strA, strB, ignoreCase) {
+  if (ignoreCase) {
+    strA = strA.toLowerCase();
+    strB = strB.toLowerCase();
+  }
+
+  // TODO: Consider other algorithms for performance
+  const strALength = strA.length;
+  const strBLength = strB.length;
+  if (strALength === 0) {
+    return strBLength;
+  }
+  if (strBLength === 0) {
+    return strALength;
+  }
+
+  const distances = new Array(strALength);
+
+  let cost, deletion, insertion, substitution, transposition;
+  for (let i = 0; i <= strALength; i++) {
+    distances[i] = new Array(strBLength);
+    distances[i][0] = i;
+    for (let j = 1; j <= strBLength; j++) {
+      if (!i) {
+        distances[0][j] = j;
+      } else {
+        cost = strA[i - 1] === strB[j - 1] ? 0 : 1;
+        deletion = distances[i - 1][j] + 1;
+        insertion = distances[i][j - 1] + 1;
+        substitution = distances[i - 1][j - 1] + cost;
+        if (deletion <= insertion && deletion <= substitution) {
+          distances[i][j] = deletion;
+        } else if (insertion <= deletion && insertion <= substitution) {
+          distances[i][j] = insertion;
+        } else {
+          distances[i][j] = substitution;
+        }
+
+        if (i > 1 && j > 1 && strA[i] === strB[j - 1] && strA[i - 1] === strB[j]) {
+          transposition = distances[i - 2][j - 2] + cost;
+          if (transposition < distances[i][j]) {
+            distances[i][j] = transposition;
+          }
+        }
+      }
+    }
+  }
+
+  return distances[strALength][strBLength];
+};
+
+const equalIgnoreCase = (a, b) => a && b && a.toLowerCase() === b.toLowerCase();
+
+const SIMPLE_TABLE_REF_SUGGESTIONS = [
+  'suggestJoinConditions',
+  'suggestAggregateFunctions',
+  'suggestFilters',
+  'suggestGroupBys',
+  'suggestOrderBys'
+];
+
+const initSqlParser = function(parser) {
+  parser.prepareNewStatement = function() {
+    linkTablePrimaries();
+    parser.commitLocations();
+
+    delete parser.yy.latestCommonTableExpressions;
+    delete parser.yy.correlatedSubQuery;
+    parser.yy.subQueries = [];
+    parser.yy.selectListAliases = [];
+    parser.yy.latestTablePrimaries = [];
+
+    prioritizeSuggestions();
+  };
+
+  parser.yy.parseError = function(message, error) {
+    parser.yy.errors.push(error);
+    return message;
+  };
+
+  parser.addCommonTableExpressions = function(identifiers) {
+    parser.yy.result.commonTableExpressions = identifiers;
+    parser.yy.latestCommonTableExpressions = identifiers;
+  };
+
+  parser.isInSubquery = function() {
+    return !!parser.yy.primariesStack.length;
+  };
+
+  parser.pushQueryState = function() {
+    parser.yy.resultStack.push(parser.yy.result);
+    parser.yy.locationsStack.push(parser.yy.locations);
+    parser.yy.selectListAliasesStack.push(parser.yy.selectListAliases);
+    parser.yy.primariesStack.push(parser.yy.latestTablePrimaries);
+    parser.yy.subQueriesStack.push(parser.yy.subQueries);
+
+    parser.yy.result = {};
+    parser.yy.locations = [];
+    parser.yy.selectListAliases = []; // Not allowed in correlated sub-queries
+
+    if (parser.yy.correlatedSubQuery) {
+      parser.yy.latestTablePrimaries = parser.yy.latestTablePrimaries.concat();
+      parser.yy.subQueries = parser.yy.subQueries.concat();
+    } else {
+      parser.yy.latestTablePrimaries = [];
+      parser.yy.subQueries = [];
+    }
+  };
+
+  parser.popQueryState = function(subQuery) {
+    linkTablePrimaries();
+    parser.commitLocations();
+
+    if (Object.keys(parser.yy.result).length === 0) {
+      parser.yy.result = parser.yy.resultStack.pop();
+    } else {
+      parser.yy.resultStack.pop();
+    }
+    const oldSubQueries = parser.yy.subQueries;
+    parser.yy.subQueries = parser.yy.subQueriesStack.pop();
+    if (subQuery) {
+      if (oldSubQueries.length > 0) {
+        subQuery.subQueries = oldSubQueries;
+      }
+      parser.yy.subQueries.push(subQuery);
+    }
+
+    parser.yy.latestTablePrimaries = parser.yy.primariesStack.pop();
+    parser.yy.locations = parser.yy.locationsStack.pop();
+    parser.yy.selectListAliases = parser.yy.selectListAliasesStack.pop();
+  };
+
+  parser.suggestSelectListAliases = function() {
+    if (
+      parser.yy.selectListAliases &&
+      parser.yy.selectListAliases.length > 0 &&
+      parser.yy.result.suggestColumns &&
+      (typeof parser.yy.result.suggestColumns.identifierChain === 'undefined' ||
+        parser.yy.result.suggestColumns.identifierChain.length === 0)
+    ) {
+      parser.yy.result.suggestColumnAliases = parser.yy.selectListAliases;
+    }
+  };
+
+  parser.mergeSuggestKeywords = function() {
+    let result = [];
+    Array.prototype.slice.call(arguments).forEach(suggestion => {
+      if (typeof suggestion !== 'undefined' && typeof suggestion.suggestKeywords !== 'undefined') {
+        result = result.concat(suggestion.suggestKeywords);
+      }
+    });
+    if (result.length > 0) {
+      return { suggestKeywords: result };
+    }
+    return {};
+  };
+
+  parser.suggestValueExpressionKeywords = function(valueExpression, extras) {
+    const expressionKeywords = parser.getValueExpressionKeywords(valueExpression, extras);
+    parser.suggestKeywords(expressionKeywords.suggestKeywords);
+    if (expressionKeywords.suggestColRefKeywords) {
+      parser.suggestColRefKeywords(expressionKeywords.suggestColRefKeywords);
+    }
+    if (valueExpression.lastType) {
+      parser.addColRefIfExists(valueExpression.lastType);
+    } else {
+      parser.addColRefIfExists(valueExpression);
+    }
+  };
+
+  parser.getSelectListKeywords = function(excludeAsterisk) {
+    let keywords = [{ value: 'CASE', weight: 450 }, 'FALSE', 'TRUE', 'NULL'];
+    if (!excludeAsterisk) {
+      keywords.push({ value: '*', weight: 10000 });
+    }
+    return keywords;
+  };
+
+  parser.getValueExpressionKeywords = function(valueExpression, extras) {
+    const types = valueExpression.lastType ? valueExpression.lastType.types : valueExpression.types;
+    // We could have valueExpression.columnReference to suggest based on column type
+    let keywords = [
+      '<',
+      '<=',
+      '<=>',
+      '<>',
+      '=',
+      '>',
+      '>=',
+      'BETWEEN',
+      'IN',
+      'IS NOT NULL',
+      'IS NULL',
+      'IS NOT TRUE',
+      'IS TRUE',
+      'IS NOT FALSE',
+      'IS FALSE',
+      'NOT BETWEEN',
+      'NOT IN'
+    ];
+    keywords = keywords.concat([
+      'IS DISTINCT FROM',
+      'IS NOT DISTINCT FROM',
+      'IS NOT UNKNOWN',
+      'IS UNKNOWN'
+    ]);
+    if (extras) {
+      keywords = keywords.concat(extras);
+    }
+    if (valueExpression.suggestKeywords) {
+      keywords = keywords.concat(valueExpression.suggestKeywords);
+    }
+    if (types.length === 1 && types[0] === 'COLREF') {
+      return {
+        suggestKeywords: keywords,
+        suggestColRefKeywords: {
+          BOOLEAN: ['AND', 'OR'],
+          NUMBER: ['+', '-', '*', '/', '%', 'DIV'],
+          STRING: ['ILIKE', 'IREGEXP', 'LIKE', 'NOT LIKE', 'REGEXP', 'RLIKE']
+        }
+      };
+    }
+    if (
+      typeof SqlFunctions === 'undefined' ||
+      SqlFunctions.matchesType(parser.yy.activeDialect, ['BOOLEAN'], types)
+    ) {
+      keywords = keywords.concat(['AND', 'OR']);
+    }
+    if (
+      typeof SqlFunctions === 'undefined' ||
+      SqlFunctions.matchesType(parser.yy.activeDialect, ['NUMBER'], types)
+    ) {
+      keywords = keywords.concat(['+', '-', '*', '/', '%', 'DIV']);
+    }
+    if (
+      typeof SqlFunctions === 'undefined' ||
+      SqlFunctions.matchesType(parser.yy.activeDialect, ['STRING'], types)
+    ) {
+      keywords = keywords.concat(['ILIKE', 'IREGEXP', 'LIKE', 'NOT LIKE', 'REGEXP', 'RLIKE']);
+    }
+    return { suggestKeywords: keywords };
+  };
+
+  parser.getTypeKeywords = function() {
+    return [
+      'BIGINT',
+      'BOOLEAN',
+      'CHAR',
+      'DECIMAL',
+      'DOUBLE',
+      'FLOAT',
+      'INT',
+      'REAL',
+      'SMALLINT',
+      'TIMESTAMP',
+      'STRING',
+      'TINYINT',
+      'VARCHAR'
+    ];
+  };
+
+  parser.getColumnDataTypeKeywords = function() {
+    return parser.getTypeKeywords().concat(['ARRAY<>', 'MAP<>', 'STRUCT<>']);
+  };
+
+  parser.addColRefIfExists = function(valueExpression) {
+    if (valueExpression.columnReference) {
+      parser.yy.result.colRef = { identifierChain: valueExpression.columnReference };
+    }
+  };
+
+  parser.selectListNoTableSuggest = function(selectListEdit, hasDistinctOrAll) {
+    if (selectListEdit.cursorAtStart) {
+      let keywords = parser.getSelectListKeywords();
+      if (!hasDistinctOrAll) {
+        keywords = keywords.concat([{ value: 'ALL', weight: 2 }, { value: 'DISTINCT', weight: 2 }]);
+      }
+      keywords.push('STRAIGHT_JOIN');
+      parser.suggestKeywords(keywords);
+    } else {
+      parser.checkForKeywords(selectListEdit);
+    }
+    if (selectListEdit.suggestFunctions) {
+      parser.suggestFunctions();
+    }
+    if (selectListEdit.suggestColumns) {
+      parser.suggestColumns();
+    }
+    if (
+      selectListEdit.suggestAggregateFunctions &&
+      (!hasDistinctOrAll || hasDistinctOrAll === 'ALL')
+    ) {
+      parser.suggestAggregateFunctions();
+      parser.suggestAnalyticFunctions();
+    }
+  };
+
+  parser.suggestJoinConditions = function(details) {
+    parser.yy.result.suggestJoinConditions = details || {};
+    if (parser.yy.latestTablePrimaries && !parser.yy.result.suggestJoinConditions.tablePrimaries) {
+      parser.yy.result.suggestJoinConditions.tablePrimaries = parser.yy.latestTablePrimaries.concat();
+    }
+  };
+
+  parser.suggestJoins = function(details) {
+    parser.yy.result.suggestJoins = details || {};
+  };
+
+  parser.valueExpressionSuggest = function(oppositeValueExpression, operator) {
+    if (oppositeValueExpression && oppositeValueExpression.columnReference) {
+      parser.suggestValues();
+      parser.yy.result.colRef = { identifierChain: oppositeValueExpression.columnReference };
+    }
+    parser.suggestColumns();
+    parser.suggestFunctions();
+    let keywords = [
+      { value: 'CASE', weight: 450 },
+      { value: 'FALSE', weight: 450 },
+      { value: 'NULL', weight: 450 },
+      { value: 'TRUE', weight: 450 }
+    ];
+    if (
+      typeof oppositeValueExpression === 'undefined' ||
+      typeof operator === 'undefined'
+    ) {
+      keywords = keywords.concat(['EXISTS', 'NOT']);
+    }
+    if (oppositeValueExpression && oppositeValueExpression.types[0] === 'NUMBER') {
+      parser.applyTypeToSuggestions(['NUMBER']);
+    } else if (typeof operator === 'undefined' || operator === '-' || operator === '+') {
+      keywords.push('INTERVAL');
+    }
+    parser.suggestKeywords(keywords);
+  };
+
+  parser.applyTypeToSuggestions = function(types) {
+    if (types[0] === 'BOOLEAN') {
+      return;
+    }
+    if (parser.yy.result.suggestFunctions && !parser.yy.result.suggestFunctions.types) {
+      parser.yy.result.suggestFunctions.types = types;
+    }
+    if (parser.yy.result.suggestColumns && !parser.yy.result.suggestColumns.types) {
+      parser.yy.result.suggestColumns.types = types;
+    }
+  };
+
+  parser.findCaseType = function(whenThenList) {
+    const types = {};
+    whenThenList.caseTypes.forEach(valueExpression => {
+      valueExpression.types.forEach(type => {
+        types[type] = true;
+      });
+    });
+    if (Object.keys(types).length === 1) {
+      return { types: [Object.keys(types)[0]] };
+    }
+    return { types: ['T'] };
+  };
+
+  parser.findReturnTypes = function(functionName) {
+    return typeof SqlFunctions === 'undefined'
+      ? ['T']
+      : SqlFunctions.getReturnTypes(parser.yy.activeDialect, functionName.toLowerCase());
+  };
+
+  parser.applyArgumentTypesToSuggestions = function(functionName, position) {
+    const foundArguments =
+      typeof SqlFunctions === 'undefined'
+        ? ['T']
+        : SqlFunctions.getArgumentTypes(
+            parser.yy.activeDialect,
+            functionName.toLowerCase(),
+            position
+          );
+    if (foundArguments.length === 0 && parser.yy.result.suggestColumns) {
+      delete parser.yy.result.suggestColumns;
+      delete parser.yy.result.suggestKeyValues;
+      delete parser.yy.result.suggestValues;
+      delete parser.yy.result.suggestFunctions;
+      delete parser.yy.result.suggestIdentifiers;
+      delete parser.yy.result.suggestKeywords;
+    } else {
+      parser.applyTypeToSuggestions(foundArguments);
+    }
+  };
+
+  const getCleanImpalaPrimaries = function(primaries) {
+    const cleanPrimaries = [];
+    for (let i = primaries.length - 1; i >= 0; i--) {
+      let cleanPrimary = primaries[i];
+      if (cleanPrimary.identifierChain && cleanPrimary.identifierChain.length > 0) {
+        for (let j = i - 1; j >= 0; j--) {
+          const parentPrimary = primaries[j];
+          if (parentPrimary.alias && cleanPrimary.identifierChain[0].name === parentPrimary.alias) {
+            const restOfChain = cleanPrimary.identifierChain.concat();
+            restOfChain.shift();
+            if (cleanPrimary.alias) {
+              cleanPrimary = {
+                identifierChain: parentPrimary.identifierChain.concat(restOfChain),
+                alias: cleanPrimary.alias,
+                impalaComplex: true
+              };
+            } else {
+              cleanPrimary = {
+                identifierChain: parentPrimary.identifierChain.concat(restOfChain),
+                impalaComplex: true
+              };
+            }
+          }
+        }
+      }
+      cleanPrimaries.push(cleanPrimary);
+    }
+    return cleanPrimaries;
+  };
+
+  parser.commitLocations = function() {
+    if (parser.yy.locations.length === 0) {
+      return;
+    }
+
+    let tablePrimaries = [];
+    getCleanImpalaPrimaries(parser.yy.latestTablePrimaries).forEach(primary => {
+      let cleanPrimary = primary;
+      if (primary.identifierChain && primary.identifierChain.length > 0) {
+        for (let j = parser.yy.primariesStack.length - 1; j >= 0; j--) {
+          getCleanImpalaPrimaries(parser.yy.primariesStack[j]).every(parentPrimary => {
+            if (parentPrimary.alias && parentPrimary.alias === primary.identifierChain[0].name) {
+              const identifierChain = primary.identifierChain.concat();
+              identifierChain.shift();
+              cleanPrimary = {
+                identifierChain: parentPrimary.identifierChain.concat(identifierChain)
+              };
+              if (primary.alias) {
+                cleanPrimary.alias = primary.alias;
+              }
+              return false;
+            }
+            return true;
+          });
+        }
+      }
+      tablePrimaries.unshift(cleanPrimary);
+    });
+    let i = parser.yy.locations.length;
+
+    while (i--) {
+      const location = parser.yy.locations[i];
+      if (location.type === 'variable' && location.colRef) {
+        parser.expandIdentifierChain({
+          wrapper: location.colRef,
+          tablePrimaries: tablePrimaries,
+          isColumnWrapper: true
+        });
+        delete location.colRef.linked;
+      }
+
+      // Impala can have references to previous tables after FROM, i.e. FROM testTable t, t.testArray
+      // In this testArray would be marked a type table so we need to switch it to column.
+      if (
+        location.type === 'table' &&
+        typeof location.identifierChain !== 'undefined' &&
+        location.identifierChain.length > 1 &&
+        tablePrimaries
+      ) {
+        let allPrimaries = tablePrimaries;
+        parser.yy.primariesStack.forEach(parentPrimaries => {
+          allPrimaries = getCleanImpalaPrimaries(parentPrimaries).concat(allPrimaries);
+        });
+        const found = allPrimaries.filter(primary => {
+          return equalIgnoreCase(primary.alias, location.identifierChain[0].name);
+        });
+        if (found.length > 0) {
+          location.type = 'column';
+        }
+      }
+
+      if (
+        location.type === 'database' &&
+        typeof location.identifierChain !== 'undefined' &&
+        location.identifierChain.length > 0 &&
+        tablePrimaries
+      ) {
+        let allPrimaries = tablePrimaries;
+        parser.yy.primariesStack.forEach(parentPrimaries => {
+          allPrimaries = getCleanImpalaPrimaries(parentPrimaries).concat(allPrimaries);
+        });
+        const foundAlias = allPrimaries.filter(primary => {
+          return equalIgnoreCase(primary.alias, location.identifierChain[0].name);
+        });
+        if (foundAlias.length > 0) {
+          // Impala complex reference in FROM clause, i.e. FROM testTable t, t.testMap tm
+          location.type = 'table';
+          parser.expandIdentifierChain({
+            tablePrimaries: allPrimaries,
+            wrapper: location,
+            anyOwner: true
+          });
+          location.type = location.identifierChain.length === 1 ? 'table' : 'complex';
+          continue;
+        }
+      }
+
+      if (location.type === 'unknown') {
+        if (
+          typeof location.identifierChain !== 'undefined' &&
+          location.identifierChain.length > 0 &&
+          location.identifierChain.length <= 2 &&
+          tablePrimaries
+        ) {
+          let found = tablePrimaries.filter(primary => {
+            return (
+              equalIgnoreCase(primary.alias, location.identifierChain[0].name) ||
+              (primary.identifierChain &&
+                equalIgnoreCase(primary.identifierChain[0].name, location.identifierChain[0].name))
+            );
+          });
+          if (!found.length && location.firstInChain) {
+            found = tablePrimaries.filter(primary => {
+              return (
+                !primary.alias &&
+                primary.identifierChain &&
+                equalIgnoreCase(
+                  primary.identifierChain[primary.identifierChain.length - 1].name,
+                  location.identifierChain[0].name
+                )
+              );
+            });
+          }
+
+          if (found.length) {
+            if (
+              found[0].identifierChain.length > 1 &&
+              location.identifierChain.length === 1 &&
+              equalIgnoreCase(found[0].identifierChain[0].name, location.identifierChain[0].name)
+            ) {
+              location.type = 'database';
+            } else if (
+              found[0].alias &&
+              equalIgnoreCase(location.identifierChain[0].name, found[0].alias) &&
+              location.identifierChain.length > 1
+            ) {
+              location.type = 'column';
+              parser.expandIdentifierChain({
+                tablePrimaries: tablePrimaries,
+                wrapper: location,
+                anyOwner: true
+              });
+            } else if (
+              !found[0].alias &&
+              found[0].identifierChain &&
+              equalIgnoreCase(
+                location.identifierChain[0].name,
+                found[0].identifierChain[found[0].identifierChain.length - 1].name
+              ) &&
+              location.identifierChain.length > 1
+            ) {
+              location.type = 'column';
+              parser.expandIdentifierChain({
+                tablePrimaries: tablePrimaries,
+                wrapper: location,
+                anyOwner: true
+              });
+            } else {
+              location.type = found[0].impalaComplex ? 'column' : 'table';
+              parser.expandIdentifierChain({
+                tablePrimaries: tablePrimaries,
+                wrapper: location,
+                anyOwner: true
+              });
+            }
+          } else if (parser.yy.subQueries) {
+            found = parser.yy.subQueries.filter(subQuery => {
+              return equalIgnoreCase(subQuery.alias, location.identifierChain[0].name);
+            });
+            if (found.length > 0) {
+              location.type = 'subQuery';
+              location.identifierChain = [{ subQuery: found[0].alias }];
+            }
+          }
+        }
+      }
+
+      if (location.type === 'asterisk' && !location.linked) {
+        if (tablePrimaries && tablePrimaries.length > 0) {
+          location.tables = [];
+          location.linked = false;
+          if (!location.identifierChain) {
+            location.identifierChain = [{ asterisk: true }];
+          }
+          parser.expandIdentifierChain({
+            tablePrimaries: tablePrimaries,
+            wrapper: location,
+            anyOwner: false
+          });
+          if (location.tables.length === 0) {
+            parser.yy.locations.splice(i, 1);
+          }
+        } else {
+          parser.yy.locations.splice(i, 1);
+        }
+      }
+
+      if (
+        location.type === 'table' &&
+        typeof location.identifierChain !== 'undefined' &&
+        location.identifierChain.length === 1 &&
+        location.identifierChain[0].name
+      ) {
+        // Could be a cte reference
+        parser.yy.locations.some(otherLocation => {
+          if (
+            otherLocation.type === 'alias' &&
+            otherLocation.source === 'cte' &&
+            identifierEquals(otherLocation.alias, location.identifierChain[0].name)
+          ) {
+            // TODO: Possibly add the other location if we want to show the link in the future.
+            //       i.e. highlight select definition on hover over alias, also for subquery references.
+            location.type = 'alias';
+            location.target = 'cte';
+            location.alias = location.identifierChain[0].name;
+            delete location.identifierChain;
+            return true;
+          }
+        });
+      }
+
+      if (
+        location.type === 'table' &&
+        (typeof location.identifierChain === 'undefined' || location.identifierChain.length === 0)
+      ) {
+        parser.yy.locations.splice(i, 1);
+      }
+
+      if (location.type === 'unknown') {
+        location.type = 'column';
+      }
+
+      // A column location might refer to a previously defined alias, i.e. last 'foo' in "SELECT cast(id AS int) foo FROM tbl ORDER BY foo;"
+      if (location.type === 'column') {
+        for (let j = i - 1; j >= 0; j--) {
+          const otherLocation = parser.yy.locations[j];
+          if (
+            otherLocation.type === 'alias' &&
+            otherLocation.source === 'column' &&
+            location.identifierChain &&
+            location.identifierChain.length === 1 &&
+            location.identifierChain[0].name &&
+            otherLocation.alias &&
+            location.identifierChain[0].name.toLowerCase() === otherLocation.alias.toLowerCase()
+          ) {
+            location.type = 'alias';
+            location.source = 'column';
+            location.alias = location.identifierChain[0].name;
+            delete location.identifierChain;
+            location.parentLocation = otherLocation.parentLocation;
+            break;
+          }
+        }
+      }
+
+      if (location.type === 'column') {
+        const initialIdentifierChain = location.identifierChain
+          ? location.identifierChain.concat()
+          : undefined;
+
+        parser.expandIdentifierChain({
+          tablePrimaries: tablePrimaries,
+          wrapper: location,
+          anyOwner: true,
+          isColumnWrapper: true,
+          isColumnLocation: true
+        });
+
+        if (typeof location.identifierChain === 'undefined') {
+          parser.yy.locations.splice(i, 1);
+        } else if (
+          location.identifierChain.length === 0 &&
+          initialIdentifierChain &&
+          initialIdentifierChain.length === 1
+        ) {
+          // This is for the case "SELECT tblOrColName FROM db.tblOrColName";
+          location.identifierChain = initialIdentifierChain;
+        }
+      }
+      if (location.type === 'column' && location.identifierChain) {
+        if (location.identifierChain.length > 1 && location.tables && location.tables.length > 0) {
+          location.type = 'complex';
+        }
+      }
+      delete location.firstInChain;
+      if (location.type !== 'column' && location.type !== 'complex') {
+        delete location.qualified;
+      } else if (typeof location.qualified === 'undefined') {
+        location.qualified = false;
+      }
+    }
+
+    if (parser.yy.locations.length > 0) {
+      parser.yy.allLocations = parser.yy.allLocations.concat(parser.yy.locations);
+      parser.yy.locations = [];
+    }
+  };
+
+  const prioritizeSuggestions = function() {
+    parser.yy.result.lowerCase = parser.yy.lowerCase || false;
+
+    const cteIndex = {};
+
+    if (typeof parser.yy.latestCommonTableExpressions !== 'undefined') {
+      parser.yy.latestCommonTableExpressions.forEach(cte => {
+        cteIndex[cte.alias.toLowerCase()] = cte;
+      });
+    }
+
+    SIMPLE_TABLE_REF_SUGGESTIONS.forEach(suggestionType => {
+      if (
+        suggestionType !== 'suggestAggregateFunctions' &&
+        typeof parser.yy.result[suggestionType] !== 'undefined' &&
+        parser.yy.result[suggestionType].tables.length === 0
+      ) {
+        delete parser.yy.result[suggestionType];
+      } else if (
+        typeof parser.yy.result[suggestionType] !== 'undefined' &&
+        typeof parser.yy.result[suggestionType].tables !== 'undefined'
+      ) {
+        for (let i = parser.yy.result[suggestionType].tables.length - 1; i >= 0; i--) {
+          const table = parser.yy.result[suggestionType].tables[i];
+          if (
+            table.identifierChain.length === 1 &&
+            typeof table.identifierChain[0].name !== 'undefined' &&
+            typeof cteIndex[table.identifierChain[0].name.toLowerCase()] !== 'undefined'
+          ) {
+            parser.yy.result[suggestionType].tables.splice(i, 1);
+          }
+        }
+      }
+    });
+
+    if (typeof parser.yy.result.colRef !== 'undefined') {
+      if (
+        !parser.yy.result.colRef.linked ||
+        typeof parser.yy.result.colRef.identifierChain === 'undefined' ||
+        parser.yy.result.colRef.identifierChain.length === 0
+      ) {
+        delete parser.yy.result.colRef;
+        if (typeof parser.yy.result.suggestColRefKeywords !== 'undefined') {
+          Object.keys(parser.yy.result.suggestColRefKeywords).forEach(type => {
+            parser.yy.result.suggestKeywords = parser.yy.result.suggestKeywords.concat(
+              parser.createWeightedKeywords(parser.yy.result.suggestColRefKeywords[type], -1)
+            );
+          });
+          delete parser.yy.result.suggestColRefKeywords;
+        }
+        if (
+          parser.yy.result.suggestColumns &&
+          parser.yy.result.suggestColumns.types.length === 1 &&
+          parser.yy.result.suggestColumns.types[0] === 'COLREF'
+        ) {
+          parser.yy.result.suggestColumns.types = ['T'];
+        }
+        delete parser.yy.result.suggestValues;
+      }
+    }
+
+    if (typeof parser.yy.result.colRef !== 'undefined') {
+      if (
+        !parser.yy.result.suggestValues &&
+        !parser.yy.result.suggestColRefKeywords &&
+        (!parser.yy.result.suggestColumns || parser.yy.result.suggestColumns.types[0] !== 'COLREF')
+      ) {
+        delete parser.yy.result.colRef;
+      }
+    }
+    if (
+      typeof parser.yy.result.suggestIdentifiers !== 'undefined' &&
+      parser.yy.result.suggestIdentifiers.length > 0
+    ) {
+      delete parser.yy.result.suggestTables;
+      delete parser.yy.result.suggestDatabases;
+    }
+    if (typeof parser.yy.result.suggestColumns !== 'undefined') {
+      const suggestColumns = parser.yy.result.suggestColumns;
+      if (typeof suggestColumns.tables === 'undefined' || suggestColumns.tables.length === 0) {
+        // Impala supports statements like SELECT * FROM tbl1, tbl2 WHERE db.tbl1.col = tbl2.bla
+        if (
+          parser.yy.result.suggestColumns.linked &&
+          typeof suggestColumns.identifierChain !== 'undefined' &&
+          suggestColumns.identifierChain.length > 0
+        ) {
+          if (suggestColumns.identifierChain.length === 1) {
+            parser.yy.result.suggestTables = suggestColumns;
+            delete parser.yy.result.suggestColumns;
+          } else {
+            suggestColumns.tables = [{ identifierChain: suggestColumns.identifierChain }];
+            delete suggestColumns.identifierChain;
+          }
+        } else {
+          delete parser.yy.result.suggestColumns;
+          delete parser.yy.result.subQueries;
+        }
+      } else {
+        delete parser.yy.result.suggestTables;
+        delete parser.yy.result.suggestDatabases;
+
+        suggestColumns.tables.forEach(table => {
+          if (
+            typeof table.identifierChain !== 'undefined' &&
+            table.identifierChain.length === 1 &&
+            typeof table.identifierChain[0].name !== 'undefined'
+          ) {
+            const cte = cteIndex[table.identifierChain[0].name.toLowerCase()];
+            if (typeof cte !== 'undefined') {
+              delete table.identifierChain[0].name;
+              table.identifierChain[0].cte = cte.alias;
+            }
+          } else if (typeof table.identifierChain === 'undefined' && table.subQuery) {
+            table.identifierChain = [{ subQuery: table.subQuery }];
+            delete table.subQuery;
+          }
+        });
+
+        if (
+          typeof suggestColumns.identifierChain !== 'undefined' &&
+          suggestColumns.identifierChain.length === 0
+        ) {
+          delete suggestColumns.identifierChain;
+        }
+      }
+    } else {
+      delete parser.yy.result.subQueries;
+    }
+
+    if (typeof parser.yy.result.suggestJoinConditions !== 'undefined') {
+      if (
+        typeof parser.yy.result.suggestJoinConditions.tables === 'undefined' ||
+        parser.yy.result.suggestJoinConditions.tables.length === 0
+      ) {
+        delete parser.yy.result.suggestJoinConditions;
+      }
+    }
+
+    if (
+      typeof parser.yy.result.suggestTables !== 'undefined' &&
+      typeof parser.yy.result.commonTableExpressions !== 'undefined'
+    ) {
+      const ctes = [];
+      parser.yy.result.commonTableExpressions.forEach(cte => {
+        const suggestion = { name: cte.alias };
+        if (parser.yy.result.suggestTables.prependFrom) {
+          suggestion.prependFrom = true;
+        }
+        if (parser.yy.result.suggestTables.prependQuestionMark) {
+          suggestion.prependQuestionMark = true;
+        }
+        ctes.push(suggestion);
+      });
+      if (ctes.length > 0) {
+        parser.yy.result.suggestCommonTableExpressions = ctes;
+      }
+    }
+  };
+
+  /**
+   * Impala supports referencing maps and arrays in the the table reference list i.e.
+   *
+   *  SELECT m['foo'].bar.| FROM someDb.someTable t, t.someMap m;
+   *
+   * From this the tablePrimaries would look like:
+   *
+   * [ { alias: 't', identifierChain: [ { name: 'someDb' }, { name: 'someTable' } ] },
+   *   { alias: 'm', identifierChain: [ { name: 't' }, { name: 'someMap' } ] } ]
+   *
+   * with an identifierChain from the select list:
+   *
+   * [ { name: 'm', keySet: true }, { name: 'bar' } ]
+   *
+   * Calling this would return an expanded identifierChain, given the above it would be:
+   *
+   * [ { name: 't' }, { name: 'someMap', keySet: true }, { name: 'bar' } ]
+   */
+  parser.expandImpalaIdentifierChain = function(tablePrimaries, identifierChain) {
+    const expandedChain = identifierChain.concat(); // Clone in case it's called multiple times.
+    if (typeof expandedChain === 'undefined' || expandedChain.length === 0) {
+      return identifierChain;
+    }
+    const expand = function(identifier, expandedChain) {
+      const foundPrimary = tablePrimaries.filter(tablePrimary => {
+        let primaryIdentifier = tablePrimary.alias;
+        if (
+          !primaryIdentifier &&
+          tablePrimary.identifierChain &&
+          tablePrimary.identifierChain.length > 0
+        ) {
+          primaryIdentifier =
+            tablePrimary.identifierChain[tablePrimary.identifierChain.length - 1].name;
+        }
+        return equalIgnoreCase(primaryIdentifier, identifier);
+      });
+
+      if (foundPrimary.length === 1 && foundPrimary[0].identifierChain) {
+        const parentPrimary = tablePrimaries.filter(tablePrimary => {
+          return equalIgnoreCase(tablePrimary.alias, foundPrimary[0].identifierChain[0].name);
+        });
+        if (parentPrimary.length === 1) {
+          const keySet = expandedChain[0].keySet;
+          const secondPart = expandedChain.slice(1);
+          const firstPart = [];
+          // Clone to make sure we don't add keySet to the primaries
+          foundPrimary[0].identifierChain.forEach(identifier => {
+            firstPart.push({ name: identifier.name });
+          });
+          if (keySet && firstPart.length > 0) {
+            firstPart[firstPart.length - 1].keySet = true;
+          }
+
+          if (
+            firstPart.length === 0 ||
+            typeof secondPart === 'undefined' ||
+            secondPart.length === 0
+          ) {
+            return firstPart;
+          }
+          const result = firstPart.concat(secondPart);
+          if (result.length > 0) {
+            return expand(firstPart[0].name, result);
+          } else {
+            return result;
+          }
+        }
+      }
+      return expandedChain;
+    };
+    return expand(expandedChain[0].name, expandedChain);
+  };
+
+  parser.identifyPartials = function(beforeCursor, afterCursor) {
+    const beforeMatch = beforeCursor.match(/[0-9a-zA-Z_]*$/);
+    const afterMatch = afterCursor.match(/^[0-9a-zA-Z_]*(?:\((?:[^)]*\))?)?/);
+    return {
+      left: beforeMatch ? beforeMatch[0].length : 0,
+      right: afterMatch ? afterMatch[0].length : 0
+    };
+  };
+
+  const addCleanTablePrimary = function(tables, tablePrimary) {
+    if (tablePrimary.alias) {
+      tables.push({ alias: tablePrimary.alias, identifierChain: tablePrimary.identifierChain });
+    } else {
+      tables.push({ identifierChain: tablePrimary.identifierChain });
+    }
+  };
+
+  parser.expandIdentifierChain = function(options) {
+    const wrapper = options.wrapper;
+    const anyOwner = options.anyOwner;
+    const isColumnWrapper = options.isColumnWrapper;
+    const isColumnLocation = options.isColumnLocation;
+    let tablePrimaries = options.tablePrimaries || parser.yy.latestTablePrimaries;
+
+    if (typeof wrapper.identifierChain === 'undefined' || typeof tablePrimaries === 'undefined') {
+      return;
+    }
+    let identifierChain = wrapper.identifierChain.concat();
+
+    if (tablePrimaries.length === 0) {
+      delete wrapper.identifierChain;
+      return;
+    }
+
+    if (!anyOwner) {
+      tablePrimaries = filterTablePrimariesForOwner(tablePrimaries, wrapper.owner);
+    }
+
+    if (identifierChain.length > 0 && identifierChain[identifierChain.length - 1].asterisk) {
+      const tables = [];
+      tablePrimaries.forEach(tablePrimary => {
+        if (identifierChain.length > 1 && !tablePrimary.subQueryAlias) {
+          if (
+            identifierChain.length === 2 &&
+            equalIgnoreCase(tablePrimary.alias, identifierChain[0].name)
+          ) {
+            addCleanTablePrimary(tables, tablePrimary);
+          } else if (
+            identifierChain.length === 2 &&
+            equalIgnoreCase(tablePrimary.identifierChain[0].name, identifierChain[0].name)
+          ) {
+            addCleanTablePrimary(tables, tablePrimary);
+          } else if (
+            identifierChain.length === 3 &&
+            tablePrimary.identifierChain.length > 1 &&
+            equalIgnoreCase(tablePrimary.identifierChain[0].name, identifierChain[0].name) &&
+            equalIgnoreCase(tablePrimary.identifierChain[1].name, identifierChain[1].name)
+          ) {
+            addCleanTablePrimary(tables, tablePrimary);
+          }
+        } else if (tablePrimary.subQueryAlias) {
+          tables.push({ identifierChain: [{ subQuery: tablePrimary.subQueryAlias }] });
+        } else {
+          addCleanTablePrimary(tables, tablePrimary);
+        }
+      });
+      // Possible Joins
+      if (tables.length > 0) {
+        wrapper.tables = tables;
+        delete wrapper.identifierChain;
+        return;
+      }
+    }
+
+    // Impala can have references to maps or array, i.e. FROM table t, t.map m
+    // We need to replace those in the identifierChain
+    const lengthBefore = identifierChain.length;
+    identifierChain = parser.expandImpalaIdentifierChain(tablePrimaries, identifierChain);
+    // Change type of any locations marked as table
+    if (wrapper.type === 'table' && identifierChain.length > lengthBefore) {
+      wrapper.type = 'column';
+    }
+    wrapper.identifierChain = identifierChain;
+
+    // IdentifierChain contains a possibly started identifier or empty, example: a.b.c = ['a', 'b', 'c']
+    // Reduce the tablePrimaries to the one that matches the first identifier if found
+    let foundPrimary;
+    let doubleMatch = false;
+    let aliasMatch = false;
+    if (identifierChain.length > 0) {
+      for (let i = 0; i < tablePrimaries.length; i++) {
+        if (tablePrimaries[i].subQueryAlias) {
+          if (equalIgnoreCase(tablePrimaries[i].subQueryAlias, identifierChain[0].name)) {
+            foundPrimary = tablePrimaries[i];
+          }
+        } else if (equalIgnoreCase(tablePrimaries[i].alias, identifierChain[0].name)) {
+          foundPrimary = tablePrimaries[i];
+          aliasMatch = true;
+          break;
+        } else if (
+          tablePrimaries[i].identifierChain.length > 1 &&
+          identifierChain.length > 1 &&
+          equalIgnoreCase(tablePrimaries[i].identifierChain[0].name, identifierChain[0].name) &&
+          equalIgnoreCase(tablePrimaries[i].identifierChain[1].name, identifierChain[1].name)
+        ) {
+          foundPrimary = tablePrimaries[i];
+          doubleMatch = true;
+          break;
+        } else if (
+          !foundPrimary &&
+          equalIgnoreCase(tablePrimaries[i].identifierChain[0].name, identifierChain[0].name) &&
+          identifierChain.length > (isColumnLocation ? 1 : 0)
+        ) {
+          foundPrimary = tablePrimaries[i];
+          // No break as first two can still match.
+        } else if (
+          !foundPrimary &&
+          tablePrimaries[i].identifierChain.length > 1 &&
+          !tablePrimaries[i].alias &&
+          equalIgnoreCase(
+            tablePrimaries[i].identifierChain[tablePrimaries[i].identifierChain.length - 1].name,
+            identifierChain[0].name
+          )
+        ) {
+          // This is for the case SELECT baa. FROM bla.baa, blo.boo;
+          foundPrimary = tablePrimaries[i];
+          break;
+        }
+      }
+    }
+
+    if (foundPrimary) {
+      if (foundPrimary.impalaComplex && wrapper.type === 'column') {
+        wrapper.type = 'complex';
+      }
+      identifierChain.shift();
+      if (doubleMatch) {
+        identifierChain.shift();
+      }
+    } else if (tablePrimaries.length === 1 && !isColumnWrapper) {
+      foundPrimary = tablePrimaries[0];
+    }
+
+    if (foundPrimary) {
+      if (isColumnWrapper) {
+        wrapper.identifierChain = identifierChain;
+        if (foundPrimary.subQueryAlias) {
+          wrapper.tables = [{ subQuery: foundPrimary.subQueryAlias }];
+        } else if (foundPrimary.alias) {
+          if (!isColumnLocation && isColumnWrapper && aliasMatch) {
+            // TODO: add alias on table in suggestColumns (needs support in sqlAutocomplete3.js)
+            // the case is: SELECT cu.| FROM customers cu;
+            // This prevents alias from being added automatically in sqlAutocompleter.js
+            wrapper.tables = [{ identifierChain: foundPrimary.identifierChain }];
+          } else {
+            wrapper.tables = [
+              { identifierChain: foundPrimary.identifierChain, alias: foundPrimary.alias }
+            ];
+          }
+        } else {
+          wrapper.tables = [{ identifierChain: foundPrimary.identifierChain }];
+        }
+      } else {
+        if (foundPrimary.subQueryAlias) {
+          identifierChain.unshift({ subQuery: foundPrimary.subQueryAlias });
+        } else {
+          identifierChain = foundPrimary.identifierChain.concat(identifierChain);
+        }
+        if (wrapper.tables) {
+          wrapper.tables.push({ identifierChain: identifierChain });
+          delete wrapper.identifierChain;
+        } else {
+          wrapper.identifierChain = identifierChain;
+        }
+      }
+    } else {
+      if (isColumnWrapper) {
+        wrapper.tables = [];
+      }
+      tablePrimaries.forEach(tablePrimary => {
+        const targetTable = tablePrimary.subQueryAlias
+          ? { subQuery: tablePrimary.subQueryAlias }
+          : { identifierChain: tablePrimary.identifierChain };
+        if (tablePrimary.alias) {
+          targetTable.alias = tablePrimary.alias;
+        }
+        if (wrapper.tables) {
+          wrapper.tables.push(targetTable);
+        }
+      });
+    }
+    delete wrapper.owner;
+    wrapper.linked = true;
+  };
+
+  const filterTablePrimariesForOwner = function(tablePrimaries, owner) {
+    const result = [];
+    tablePrimaries.forEach(primary => {
+      if (typeof owner === 'undefined' && typeof primary.owner === 'undefined') {
+        result.push(primary);
+      } else if (owner === primary.owner) {
+        result.push(primary);
+      }
+    });
+    return result;
+  };
+
+  const convertTablePrimariesToSuggestions = function(tablePrimaries) {
+    const tables = [];
+    const identifiers = [];
+    tablePrimaries.forEach(tablePrimary => {
+      if (tablePrimary.identifierChain && tablePrimary.identifierChain.length > 0) {
+        const table = { identifierChain: tablePrimary.identifierChain };
+        if (tablePrimary.alias) {
+          table.alias = tablePrimary.alias;
+          identifiers.push({ name: table.alias + '.', type: 'alias' });
+          const testForImpalaAlias = [{ name: table.alias }];
+          const result = parser.expandImpalaIdentifierChain(tablePrimaries, testForImpalaAlias);
+          if (result.length > 1) {
+            // Continue if it's a reference to a complex type
+            return;
+          }
+        } else {
+          const lastIdentifier =
+            tablePrimary.identifierChain[tablePrimary.identifierChain.length - 1];
+          if (typeof lastIdentifier.name !== 'undefined') {
+            identifiers.push({ name: lastIdentifier.name + '.', type: 'table' });
+          } else if (typeof lastIdentifier.subQuery !== 'undefined') {
+            identifiers.push({ name: lastIdentifier.subQuery + '.', type: 'sub-query' });
+          }
+        }
+        tables.push(table);
+      } else if (tablePrimary.subQueryAlias) {
+        identifiers.push({ name: tablePrimary.subQueryAlias + '.', type: 'sub-query' });
+        tables.push({ identifierChain: [{ subQuery: tablePrimary.subQueryAlias }] });
+      }
+    });
+    if (identifiers.length > 0) {
+      if (typeof parser.yy.result.suggestIdentifiers === 'undefined') {
+        parser.yy.result.suggestIdentifiers = identifiers;
+      } else {
+        parser.yy.result.suggestIdentifiers = identifiers.concat(
+          parser.yy.result.suggestIdentifiers
+        );
+      }
+    }
+    parser.yy.result.suggestColumns.tables = tables;
+    if (
+      parser.yy.result.suggestColumns.identifierChain &&
+      parser.yy.result.suggestColumns.identifierChain.length === 0
+    ) {
+      delete parser.yy.result.suggestColumns.identifierChain;
+    }
+    parser.yy.result.suggestColumns.linked = true;
+  };
+
+  const linkTablePrimaries = function() {
+    if (!parser.yy.cursorFound || typeof parser.yy.latestTablePrimaries === 'undefined') {
+      return;
+    }
+
+    SIMPLE_TABLE_REF_SUGGESTIONS.forEach(suggestionType => {
+      if (
+        typeof parser.yy.result[suggestionType] !== 'undefined' &&
+        parser.yy.result[suggestionType].tablePrimaries &&
+        !parser.yy.result[suggestionType].linked
+      ) {
+        parser.yy.result[suggestionType].tables = [];
+        parser.yy.result[suggestionType].tablePrimaries.forEach(tablePrimary => {
+          if (!tablePrimary.subQueryAlias) {
+            parser.yy.result[suggestionType].tables.push(
+              tablePrimary.alias
+                ? {
+                    identifierChain: tablePrimary.identifierChain.concat(),
+                    alias: tablePrimary.alias
+                  }
+                : { identifierChain: tablePrimary.identifierChain.concat() }
+            );
+          }
+        });
+        delete parser.yy.result[suggestionType].tablePrimaries;
+        parser.yy.result[suggestionType].linked = true;
+      }
+    });
+
+    if (
+      typeof parser.yy.result.suggestColumns !== 'undefined' &&
+      !parser.yy.result.suggestColumns.linked
+    ) {
+      const tablePrimaries = filterTablePrimariesForOwner(
+        parser.yy.latestTablePrimaries,
+        parser.yy.result.suggestColumns.owner
+      );
+      if (!parser.yy.result.suggestColumns.tables) {
+        parser.yy.result.suggestColumns.tables = [];
+      }
+      if (parser.yy.subQueries.length > 0) {
+        parser.yy.result.subQueries = parser.yy.subQueries;
+      }
+      if (
+        typeof parser.yy.result.suggestColumns.identifierChain === 'undefined' ||
+        parser.yy.result.suggestColumns.identifierChain.length === 0
+      ) {
+        if (tablePrimaries.length > 1) {
+          convertTablePrimariesToSuggestions(tablePrimaries);
+        } else {
+          if (
+            tablePrimaries.length === 1 &&
+            (tablePrimaries[0].alias || tablePrimaries[0].subQueryAlias)
+          ) {
+            convertTablePrimariesToSuggestions(tablePrimaries);
+          }
+          parser.expandIdentifierChain({
+            wrapper: parser.yy.result.suggestColumns,
+            anyOwner: false,
+            isColumnWrapper: true
+          });
+        }
+      } else {
+        parser.expandIdentifierChain({
+          wrapper: parser.yy.result.suggestColumns,
+          anyOwner: false,
+          isColumnWrapper: true
+        });
+      }
+    }
+
+    if (typeof parser.yy.result.colRef !== 'undefined' && !parser.yy.result.colRef.linked) {
+      parser.expandIdentifierChain({ wrapper: parser.yy.result.colRef });
+
+      const primaries = filterTablePrimariesForOwner(parser.yy.latestTablePrimaries);
+      if (
+        primaries.length === 0 ||
+        (primaries.length > 1 && parser.yy.result.colRef.identifierChain.length === 1)
+      ) {
+        parser.yy.result.colRef.identifierChain = [];
+      }
+    }
+    if (
+      typeof parser.yy.result.suggestKeyValues !== 'undefined' &&
+      !parser.yy.result.suggestKeyValues.linked
+    ) {
+      parser.expandIdentifierChain({ wrapper: parser.yy.result.suggestKeyValues });
+    }
+  };
+
+  parser.getSubQuery = function(cols) {
+    const columns = [];
+    cols.selectList.forEach(col => {
+      const result = {};
+      if (col.alias) {
+        result.alias = col.alias;
+      }
+      if (col.valueExpression && col.valueExpression.columnReference) {
+        result.identifierChain = col.valueExpression.columnReference;
+      } else if (col.asterisk) {
+        result.identifierChain = [{ asterisk: true }];
+      }
+      if (
+        col.valueExpression &&
+        col.valueExpression.types &&
+        col.valueExpression.types.length === 1
+      ) {
+        result.type = col.valueExpression.types[0];
+      }
+
+      columns.push(result);
+    });
+
+    return {
+      columns: columns
+    };
+  };
+
+  parser.addTablePrimary = function(ref) {
+    if (typeof parser.yy.latestTablePrimaries === 'undefined') {
+      parser.yy.latestTablePrimaries = [];
+    }
+    parser.yy.latestTablePrimaries.push(ref);
+  };
+
+  parser.suggestFileFormats = function() {
+    parser.suggestKeywords([
+      'AVRO',
+      'KUDU',
+      'ORC',
+      'PARQUET',
+      'RCFILE',
+      'SEQUENCEFILE',
+      'TEXTFILE'
+    ]);
+  };
+
+  parser.getKeywordsForOptionalsLR = function(optionals, keywords, override) {
+    let result = [];
+
+    for (let i = 0; i < optionals.length; i++) {
+      if (!optionals[i] && (typeof override === 'undefined' || override[i])) {
+        if (keywords[i] instanceof Array) {
+          result = result.concat(keywords[i]);
+        } else {
+          result.push(keywords[i]);
+        }
+      } else if (optionals[i]) {
+        break;
+      }
+    }
+    return result;
+  };
+
+  parser.suggestDdlAndDmlKeywords = function(extraKeywords) {
+    let keywords = [
+      'ALTER',
+      'CREATE',
+      'DESCRIBE',
+      'DROP',
+      'GRANT',
+      'INSERT',
+      'REVOKE',
+      'SELECT',
+      'SET',
+      'SHOW',
+      'TRUNCATE',
+      'UPDATE',
+      'USE',
+      'WITH'
+    ];
+
+    if (extraKeywords) {
+      keywords = keywords.concat(extraKeywords);
+    }
+
+    keywords = keywords.concat([
+      'COMMENT ON',
+      'COMPUTE',
+      'DELETE',
+      'INVALIDATE METADATA',
+      'LOAD',
+      'REFRESH',
+      'UPSERT'
+    ]);
+
+    parser.suggestKeywords(keywords);
+  };
+
+  parser.checkForSelectListKeywords = function(selectList) {
+    if (selectList.length === 0) {
+      return;
+    }
+    const last = selectList[selectList.length - 1];
+    if (!last || !last.valueExpression) {
+      return;
+    }
+    const valueExpressionKeywords = parser.getValueExpressionKeywords(last.valueExpression);
+    let keywords = [];
+    if (last.suggestKeywords) {
+      keywords = keywords.concat(last.suggestKeywords);
+    }
+    if (valueExpressionKeywords.suggestKeywords) {
+      keywords = keywords.concat(valueExpressionKeywords.suggestKeywords);
+    }
+    if (valueExpressionKeywords.suggestColRefKeywords) {
+      parser.suggestColRefKeywords(valueExpressionKeywords.suggestColRefKeywords);
+      parser.addColRefIfExists(last.valueExpression);
+    }
+    if (!last.alias) {
+      keywords.push('AS');
+    }
+    if (keywords.length > 0) {
+      parser.suggestKeywords(keywords);
+    }
+  };
+
+  parser.checkForKeywords = function(expression) {
+    if (expression) {
+      if (expression.suggestKeywords && expression.suggestKeywords.length > 0) {
+        parser.suggestKeywords(expression.suggestKeywords);
+      }
+      if (expression.suggestColRefKeywords) {
+        parser.suggestColRefKeywords(expression.suggestColRefKeywords);
+        parser.addColRefIfExists(expression);
+      }
+    }
+  };
+
+  parser.createWeightedKeywords = function(keywords, weight) {
+    const result = [];
+    keywords.forEach(keyword => {
+      if (typeof keyword.weight !== 'undefined') {
+        keyword.weight = weight + keyword.weight / 10;
+        result.push(keyword);
+      } else {
+        result.push({ value: keyword, weight: weight });
+      }
+    });
+    return result;
+  };
+
+  parser.suggestKeywords = function(keywords) {
+    const weightedKeywords = [];
+    if (keywords.length === 0) {
+      return;
+    }
+    keywords.forEach(keyword => {
+      if (typeof keyword.weight !== 'undefined') {
+        weightedKeywords.push(keyword);
+      } else {
+        weightedKeywords.push({ value: keyword, weight: -1 });
+      }
+    });
+    weightedKeywords.sort((a, b) => {
+      if (a.weight !== b.weight) {
+        return b.weight - a.weight;
+      }
+      return a.value.localeCompare(b.value);
+    });
+    parser.yy.result.suggestKeywords = weightedKeywords;
+  };
+
+  parser.suggestColRefKeywords = function(colRefKeywords) {
+    parser.yy.result.suggestColRefKeywords = colRefKeywords;
+  };
+
+  parser.suggestTablesOrColumns = function(identifier) {
+    if (typeof parser.yy.latestTablePrimaries == 'undefined') {
+      parser.suggestTables({ identifierChain: [{ name: identifier }] });
+      return;
+    }
+    const tableRef = parser.yy.latestTablePrimaries.filter(tablePrimary => {
+      return equalIgnoreCase(tablePrimary.alias, identifier);
+    });
+    if (tableRef.length > 0) {
+      parser.suggestColumns({ identifierChain: [{ name: identifier }] });
+    } else {
+      parser.suggestTables({ identifierChain: [{ name: identifier }] });
+    }
+  };
+
+  parser.suggestFunctions = function(details) {
+    parser.yy.result.suggestFunctions = details || {};
+  };
+
+  parser.suggestAggregateFunctions = function() {
+    const primaries = [];
+    const aliases = {};
+    parser.yy.latestTablePrimaries.forEach(primary => {
+      if (typeof primary.alias !== 'undefined') {
+        aliases[primary.alias] = true;
+      }
+      // Drop if the first one refers to a table alias (...FROM tbl t, t.map tm ...)
+      if (
+        typeof primary.identifierChain !== 'undefined' &&
+        !aliases[primary.identifierChain[0].name] &&
+        typeof primary.owner === 'undefined'
+      ) {
+        primaries.push(primary);
+      }
+    });
+    parser.yy.result.suggestAggregateFunctions = { tablePrimaries: primaries };
+  };
+
+  parser.suggestAnalyticFunctions = function() {
+    parser.yy.result.suggestAnalyticFunctions = true;
+  };
+
+  parser.suggestSetOptions = function() {
+    parser.yy.result.suggestSetOptions = true;
+  };
+
+  parser.suggestIdentifiers = function(identifiers) {
+    parser.yy.result.suggestIdentifiers = identifiers;
+  };
+
+  parser.suggestColumns = function(details) {
+    if (typeof details === 'undefined') {
+      details = { identifierChain: [] };
+    } else if (typeof details.identifierChain === 'undefined') {
+      details.identifierChain = [];
+    }
+    parser.yy.result.suggestColumns = details;
+  };
+
+  parser.suggestGroupBys = function(details) {
+    parser.yy.result.suggestGroupBys = details || {};
+  };
+
+  parser.suggestOrderBys = function(details) {
+    parser.yy.result.suggestOrderBys = details || {};
+  };
+
+  parser.suggestFilters = function(details) {
+    parser.yy.result.suggestFilters = details || {};
+  };
+
+  parser.suggestKeyValues = function(details) {
+    parser.yy.result.suggestKeyValues = details || {};
+  };
+
+  parser.suggestTables = function(details) {
+    parser.yy.result.suggestTables = details || {};
+  };
+
+  const adjustLocationForCursor = function(location) {
+    // columns are 0-based and lines not, so add 1 to cols
+    const newLocation = {
+      first_line: location.first_line,
+      last_line: location.last_line,
+      first_column: location.first_column + 1,
+      last_column: location.last_column + 1
+    };
+    if (parser.yy.cursorFound) {
+      if (
+        parser.yy.cursorFound.first_line === newLocation.first_line &&
+        parser.yy.cursorFound.last_column <= newLocation.first_column
+      ) {
+        let additionalSpace = parser.yy.partialLengths.left + parser.yy.partialLengths.right;
+        additionalSpace -= parser.yy.partialCursor ? 1 : 3; // For some reason the normal cursor eats 3 positions.
+        newLocation.first_column = newLocation.first_column + additionalSpace;
+        newLocation.last_column = newLocation.last_column + additionalSpace;
+      }
+    }
+    return newLocation;
+  };
+
+  parser.addFunctionLocation = function(location, functionName) {
+    // Remove trailing '(' from location
+    const adjustedLocation = {
+      first_line: location.first_line,
+      last_line: location.last_line,
+      first_column: location.first_column,
+      last_column: location.last_column - 1
+    };
+    parser.yy.locations.push({
+      type: 'function',
+      location: adjustLocationForCursor(adjustedLocation),
+      function: functionName.toLowerCase()
+    });
+  };
+
+  parser.addStatementLocation = function(location) {
+    // Don't report lonely cursor as a statement
+    if (
+      location.first_line === location.last_line &&
+      Math.abs(location.last_column - location.first_column) === 1
+    ) {
+      return;
+    }
+    let adjustedLocation;
+    if (
+      parser.yy.cursorFound &&
+      parser.yy.cursorFound.last_line === location.last_line &&
+      parser.yy.cursorFound.first_column >= location.first_column &&
+      parser.yy.cursorFound.last_column <= location.last_column
+    ) {
+      const additionalSpace = parser.yy.partialLengths.left + parser.yy.partialLengths.right;
+      adjustedLocation = {
+        first_line: location.first_line,
+        last_line: location.last_line,
+        first_column: location.first_column + 1,
+        last_column: location.last_column + additionalSpace - (parser.yy.partialCursor ? 0 : 2)
+      };
+    } else {
+      adjustedLocation = {
+        first_line: location.first_line,
+        last_line: location.last_line,
+        first_column: location.first_column + 1,
+        last_column: location.last_column + 1
+      };
+    }
+
+    parser.yy.locations.push({
+      type: 'statement',
+      location: adjustedLocation
+    });
+  };
+
+  parser.firstDefined = function() {
+    for (let i = 0; i + 1 < arguments.length; i += 2) {
+      if (arguments[i]) {
+        return arguments[i + 1];
+      }
+    }
+  };
+
+  parser.addClauseLocation = function(type, precedingLocation, locationIfPresent, isCursor) {
+    let location;
+    if (isCursor) {
+      if (parser.yy.partialLengths.left === 0 && parser.yy.partialLengths.right === 0) {
+        location = {
+          type: type,
+          missing: true,
+          location: adjustLocationForCursor({
+            first_line: precedingLocation.last_line,
+            first_column: precedingLocation.last_column,
+            last_line: precedingLocation.last_line,
+            last_column: precedingLocation.last_column
+          })
+        };
+      } else {
+        location = {
+          type: type,
+          missing: false,
+          location: {
+            first_line: locationIfPresent.last_line,
+            first_column: locationIfPresent.last_column - 1,
+            last_line: locationIfPresent.last_line,
+            last_column:
+              locationIfPresent.last_column -
+              1 +
+              parser.yy.partialLengths.right +
+              parser.yy.partialLengths.left
+          }
+        };
+      }
+    } else {
+      location = {
+        type: type,
+        missing: !locationIfPresent,
+        location: adjustLocationForCursor(
+          locationIfPresent || {
+            first_line: precedingLocation.last_line,
+            first_column: precedingLocation.last_column,
+            last_line: precedingLocation.last_line,
+            last_column: precedingLocation.last_column
+          }
+        )
+      };
+    }
+    if (parser.isInSubquery()) {
+      location.subquery = true;
+    }
+    parser.yy.locations.push(location);
+  };
+
+  parser.addStatementTypeLocation = function(identifier, location, additionalText) {
+    // Don't add if already there except for SELECT
+    if (identifier !== 'SELECT' && parser.yy.allLocations) {
+      for (let i = parser.yy.allLocations.length - 1; i >= 0; i--) {
+        if (parser.yy.allLocations[i] && parser.yy.allLocations[i].type === 'statement') {
+          break;
+        }
+        if (parser.yy.allLocations[i] && parser.yy.allLocations[i].type === 'statementType') {
+          return;
+        }
+      }
+    }
+    const loc = {
+      type: 'statementType',
+      location: adjustLocationForCursor(location),
+      identifier: identifier
+    };
+    if (typeof additionalText !== 'undefined') {
+      switch (identifier) {
+        case 'ALTER':
+          if (/ALTER\s+VIEW/i.test(additionalText)) {
+            loc.identifier = 'ALTER VIEW';
+          } else {
+            loc.identifier = 'ALTER TABLE';
+          }
+          break;
+        case 'COMPUTE':
+          loc.identifier = 'COMPUTE STATS';
+          break;
+        case 'CREATE':
+          if (/CREATE\s+VIEW/i.test(additionalText)) {
+            loc.identifier = 'CREATE VIEW';
+          } else if (/CREATE\s+TABLE/i.test(additionalText)) {
+            loc.identifier = 'CREATE TABLE';
+          } else if (/CREATE\s+DATABASE/i.test(additionalText)) {
+            loc.identifier = 'CREATE DATABASE';
+          } else if (/CREATE\s+ROLE/i.test(additionalText)) {
+            loc.identifier = 'CREATE ROLE';
+          } else if (/CREATE\s+FUNCTION/i.test(additionalText)) {
+            loc.identifier = 'CREATE FUNCTION';
+          } else {
+            loc.identifier = 'CREATE TABLE';
+          }
+          break;
+        case 'DROP':
+          if (/DROP\s+VIEW/i.test(additionalText)) {
+            loc.identifier = 'DROP VIEW';
+          } else if (/DROP\s+TABLE/i.test(additionalText)) {
+            loc.identifier = 'DROP TABLE';
+          } else if (/DROP\s+DATABASE/i.test(additionalText)) {
+            loc.identifier = 'DROP DATABASE';
+          } else if (/DROP\s+ROLE/i.test(additionalText)) {
+            loc.identifier = 'DROP ROLE';
+          } else if (/DROP\s+STATS/i.test(additionalText)) {
+            loc.identifier = 'DROP STATS';
+          } else if (/DROP\s+FUNCTION/i.test(additionalText)) {
+            loc.identifier = 'DROP FUNCTION';
+          } else {
+            loc.identifier = 'DROP TABLE';
+          }
+          break;
+        case 'INVALIDATE':
+          loc.identifier = 'INVALIDATE METADATA';
+          break;
+        case 'LOAD':
+          loc.identifier = 'LOAD DATA';
+          break;
+        case 'TRUNCATE':
+          loc.identifier = 'TRUNCATE TABLE';
+          break;
+        default:
+      }
+    }
+    parser.yy.locations.push(loc);
+  };
+
+  parser.addFileLocation = function(location, path) {
+    parser.yy.locations.push({
+      type: 'file',
+      location: adjustLocationForCursor(location),
+      path: path
+    });
+  };
+
+  parser.addDatabaseLocation = function(location, identifierChain) {
+    parser.yy.locations.push({
+      type: 'database',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addTableLocation = function(location, identifierChain) {
+    parser.yy.locations.push({
+      type: 'table',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addColumnAliasLocation = function(location, alias, parentLocation) {
+    const aliasLocation = {
+      type: 'alias',
+      source: 'column',
+      alias: alias,
+      location: adjustLocationForCursor(location),
+      parentLocation: adjustLocationForCursor(parentLocation)
+    };
+    if (
+      parser.yy.locations.length &&
+      parser.yy.locations[parser.yy.locations.length - 1].type === 'column'
+    ) {
+      const closestColumn = parser.yy.locations[parser.yy.locations.length - 1];
+      if (
+        closestColumn.location.first_line === aliasLocation.parentLocation.first_line &&
+        closestColumn.location.last_line === aliasLocation.parentLocation.last_line &&
+        closestColumn.location.first_column === aliasLocation.parentLocation.first_column &&
+        closestColumn.location.last_column === aliasLocation.parentLocation.last_column
+      ) {
+        parser.yy.locations[parser.yy.locations.length - 1].alias = alias;
+      }
+    }
+    parser.yy.locations.push(aliasLocation);
+  };
+
+  parser.addTableAliasLocation = function(location, alias, identifierChain) {
+    parser.yy.locations.push({
+      type: 'alias',
+      source: 'table',
+      alias: alias,
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addSubqueryAliasLocation = function(location, alias) {
+    parser.yy.locations.push({
+      type: 'alias',
+      source: 'subquery',
+      alias: alias,
+      location: adjustLocationForCursor(location)
+    });
+  };
+
+  parser.addAsteriskLocation = function(location, identifierChain) {
+    parser.yy.locations.push({
+      type: 'asterisk',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addVariableLocation = function(location, value) {
+    if (/\${[^}]*}/.test(value)) {
+      parser.yy.locations.push({
+        type: 'variable',
+        location: adjustLocationForCursor(location),
+        value: value
+      });
+    }
+  };
+
+  parser.addColumnLocation = function(location, identifierChain) {
+    const isVariable =
+      identifierChain.length && /\${[^}]*}/.test(identifierChain[identifierChain.length - 1].name);
+    if (isVariable) {
+      parser.yy.locations.push({
+        type: 'variable',
+        location: adjustLocationForCursor(location),
+        value: identifierChain[identifierChain.length - 1].name
+      });
+    } else {
+      parser.yy.locations.push({
+        type: 'column',
+        location: adjustLocationForCursor(location),
+        identifierChain: identifierChain,
+        qualified: identifierChain.length > 1
+      });
+    }
+  };
+
+  parser.addCteAliasLocation = function(location, alias) {
+    parser.yy.locations.push({
+      type: 'alias',
+      source: 'cte',
+      alias: alias,
+      location: adjustLocationForCursor(location)
+    });
+  };
+
+  parser.addUnknownLocation = function(location, identifierChain) {
+    const isVariable =
+      identifierChain.length && /\${[^}]*}/.test(identifierChain[identifierChain.length - 1].name);
+    let loc;
+    if (isVariable) {
+      loc = {
+        type: 'variable',
+        location: adjustLocationForCursor(location),
+        value: identifierChain[identifierChain.length - 1].name
+      };
+    } else {
+      loc = {
+        type: 'unknown',
+        location: adjustLocationForCursor(location),
+        identifierChain: identifierChain,
+        qualified: identifierChain.length > 1
+      };
+    }
+    parser.yy.locations.push(loc);
+    return loc;
+  };
+
+  parser.addNewDatabaseLocation = function(location, identifierChain) {
+    parser.yy.definitions.push({
+      type: 'database',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addNewTableLocation = function(location, identifierChain, colSpec) {
+    const columns = [];
+    if (colSpec) {
+      colSpec.forEach(col => {
+        columns.push({
+          identifierChain: [col.identifier], // TODO: Complex
+          type: col.type,
+          location: adjustLocationForCursor(col.location)
+        });
+      });
+    }
+    parser.yy.definitions.push({
+      type: 'table',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain,
+      columns: columns
+    });
+  };
+
+  parser.addColRefToVariableIfExists = function(left, right) {
+    if (
+      left &&
+      left.columnReference &&
+      left.columnReference.length &&
+      right &&
+      right.columnReference &&
+      right.columnReference.length &&
+      parser.yy.locations.length > 1
+    ) {
+      const addColRefToVariableLocation = function(variableValue, colRef) {
+        // See if colref is actually an alias
+        if (colRef.length === 1 && colRef[0].name) {
+          parser.yy.locations.some(location => {
+            if (location.type === 'column' && location.alias === colRef[0].name) {
+              colRef = location.identifierChain;
+              return true;
+            }
+          });
+        }
+
+        for (let i = parser.yy.locations.length - 1; i > 0; i--) {
+          const location = parser.yy.locations[i];
+          if (location.type === 'variable' && location.value === variableValue) {
+            location.colRef = { identifierChain: colRef };
+            break;
+          }
+        }
+      };
+
+      if (/\${[^}]*}/.test(left.columnReference[0].name)) {
+        // left is variable
+        addColRefToVariableLocation(left.columnReference[0].name, right.columnReference);
+      } else if (/\${[^}]*}/.test(right.columnReference[0].name)) {
+        // right is variable
+        addColRefToVariableLocation(right.columnReference[0].name, left.columnReference);
+      }
+    }
+  };
+
+  parser.suggestDatabases = function(details) {
+    parser.yy.result.suggestDatabases = details || {};
+  };
+
+  parser.suggestHdfs = function(details) {
+    parser.yy.result.suggestHdfs = details || {};
+  };
+
+  parser.suggestValues = function(details) {
+    parser.yy.result.suggestValues = details || {};
+  };
+
+  parser.determineCase = function(text) {
+    if (!parser.yy.caseDetermined) {
+      parser.yy.lowerCase = text.toLowerCase() === text;
+      parser.yy.caseDetermined = true;
+    }
+  };
+
+  parser.handleQuotedValueWithCursor = function(lexer, yytext, yylloc, quoteChar) {
+    if (yytext.indexOf('\u2020') !== -1 || yytext.indexOf('\u2021') !== -1) {
+      parser.yy.partialCursor = yytext.indexOf('\u2021') !== -1;
+      const cursorIndex = parser.yy.partialCursor
+        ? yytext.indexOf('\u2021')
+        : yytext.indexOf('\u2020');
+      parser.yy.cursorFound = {
+        first_line: yylloc.first_line,
+        last_line: yylloc.last_line,
+        first_column: yylloc.first_column + cursorIndex,
+        last_column: yylloc.first_column + cursorIndex + 1
+      };
+      const remainder = yytext.substring(cursorIndex + 1);
+      const remainingQuotes = (lexer.upcomingInput().match(new RegExp(quoteChar, 'g')) || [])
+        .length;
+      if (remainingQuotes > 0 && (remainingQuotes & 1) !== 0) {
+        parser.yy.missingEndQuote = false;
+        lexer.input();
+      } else {
+        parser.yy.missingEndQuote = true;
+        lexer.unput(remainder);
+      }
+      lexer.popState();
+      return true;
+    }
+    return false;
+  };
+
+  let lexerModified = false;
+
+  /**
+   * Main parser function
+   */
+  parser.parseSql = function(beforeCursor, afterCursor, debug) {
+    // Jison counts CRLF as two lines in the locations
+    beforeCursor = beforeCursor.replace(/\r\n|\n\r/gm, '\n');
+    afterCursor = afterCursor.replace(/\r\n|\n\r/gm, '\n');
+    parser.yy.result = { locations: [] };
+    parser.yy.lowerCase = false;
+    parser.yy.locations = [];
+    parser.yy.definitions = [];
+    parser.yy.allLocations = [];
+    parser.yy.subQueries = [];
+    parser.yy.errors = [];
+    parser.yy.selectListAliases = [];
+
+    parser.yy.locationsStack = [];
+    parser.yy.primariesStack = [];
+    parser.yy.subQueriesStack = [];
+    parser.yy.resultStack = [];
+    parser.yy.selectListAliasesStack = [];
+    parser.yy.activeDialect = 'impala';
+
+    delete parser.yy.caseDetermined;
+    delete parser.yy.cursorFound;
+    delete parser.yy.partialCursor;
+
+    parser.prepareNewStatement();
+
+    const REASONABLE_SURROUNDING_LENGTH = 150000; // About 3000 lines before and after
+
+    if (beforeCursor.length > REASONABLE_SURROUNDING_LENGTH) {
+      if (beforeCursor.length - beforeCursor.lastIndexOf(';') > REASONABLE_SURROUNDING_LENGTH) {
+        // Bail out if the last complete statement is more than 150000 chars before
+        return {};
+      }
+      // Cut it at the first statement found within 150000 chars before
+      const lastReasonableChunk = beforeCursor.substring(
+        beforeCursor.length - REASONABLE_SURROUNDING_LENGTH
+      );
+      beforeCursor = lastReasonableChunk.substring(lastReasonableChunk.indexOf(';') + 1);
+    }
+
+    if (afterCursor.length > REASONABLE_SURROUNDING_LENGTH) {
+      if (afterCursor.length - afterCursor.indexOf(';') > REASONABLE_SURROUNDING_LENGTH) {
+        // No need to bail out for what's comes after, we can still get keyword completion
+        afterCursor = '';
+      } else {
+        // Cut it at the last statement found within 150000 chars after
+        const firstReasonableChunk = afterCursor.substring(0, REASONABLE_SURROUNDING_LENGTH);
+        afterCursor = firstReasonableChunk.substring(0, firstReasonableChunk.lastIndexOf(';'));
+      }
+    }
+
+    parser.yy.partialLengths = parser.identifyPartials(beforeCursor, afterCursor);
+
+    if (parser.yy.partialLengths.left > 0) {
+      beforeCursor = beforeCursor.substring(0, beforeCursor.length - parser.yy.partialLengths.left);
+    }
+
+    if (parser.yy.partialLengths.right > 0) {
+      afterCursor = afterCursor.substring(parser.yy.partialLengths.right);
+    }
+
+    let result;
+    try {
+      // Add |CURSOR| or |PARTIAL_CURSOR| to represent the different cursor states in the lexer
+      result = parser.parse(
+        beforeCursor +
+          (beforeCursor.length === 0 || /[\s(]$/.test(beforeCursor) ? ' \u2020 ' : '\u2021') +
+          afterCursor
+      );
+    } catch (err) {
+      // On any error try to at least return any existing result
+      if (typeof parser.yy.result === 'undefined') {
+        throw err;
+      }
+      if (debug) {
+        console.log(err);
+        console.error(err.stack);
+      }
+      result = parser.yy.result;
+    }
+    if (parser.yy.errors.length > 0) {
+      parser.yy.result.errors = parser.yy.errors;
+      if (debug) {
+        console.log(parser.yy.errors);
+      }
+    }
+    try {
+      linkTablePrimaries();
+      parser.commitLocations();
+      // Clean up and prioritize
+      prioritizeSuggestions();
+    } catch (err) {
+      if (debug) {
+        console.log(err);
+        console.error(err.stack);
+      }
+    }
+
+    parser.yy.allLocations.sort((a, b) => {
+      if (a.location.first_line !== b.location.first_line) {
+        return a.location.first_line - b.location.first_line;
+      }
+      if (a.location.first_column !== b.location.first_column) {
+        return a.location.first_column - b.location.first_column;
+      }
+      if (a.location.last_column !== b.location.last_column) {
+        return b.location.last_column - a.location.last_column;
+      }
+      return b.type.localeCompare(a.type);
+    });
+    parser.yy.result.locations = parser.yy.allLocations;
+    parser.yy.result.definitions = parser.yy.definitions;
+
+    parser.yy.result.locations.forEach(location => {
+      delete location.linked;
+    });
+    if (typeof parser.yy.result.suggestColumns !== 'undefined') {
+      delete parser.yy.result.suggestColumns.linked;
+    }
+
+    SIMPLE_TABLE_REF_SUGGESTIONS.forEach(suggestionType => {
+      if (typeof parser.yy.result[suggestionType] !== 'undefined') {
+        delete parser.yy.result[suggestionType].linked;
+      }
+    });
+
+    if (typeof parser.yy.result.colRef !== 'undefined') {
+      delete parser.yy.result.colRef.linked;
+    }
+    if (typeof parser.yy.result.suggestKeyValues !== 'undefined') {
+      delete parser.yy.result.suggestKeyValues.linked;
+    }
+
+    if (typeof result.error !== 'undefined' && typeof result.error.expected !== 'undefined') {
+      // Remove the cursor from expected tokens
+      result.error.expected = result.error.expected.filter(token => token.indexOf('CURSOR') === -1)
+    }
+
+    if (typeof result.error !== 'undefined' && result.error.recoverable) {
+      delete result.error;
+    }
+
+    // Adjust all the statement locations to include white space surrounding them
+    let lastStatementLocation = null;
+    result.locations.forEach(location => {
+      if (location.type === 'statement') {
+        if (lastStatementLocation === null) {
+          location.location.first_line = 1;
+          location.location.first_column = 1;
+        } else {
+          location.location.first_line = lastStatementLocation.location.last_line;
+          location.location.first_column = lastStatementLocation.location.last_column + 1;
+        }
+        lastStatementLocation = location;
+      }
+    });
+
+    return result;
+  };
+};
+
+const SYNTAX_PARSER_NOOP_FUNCTIONS = [
+  'addAsteriskLocation',
+  'addClauseLocation',
+  'addColRefIfExists',
+  'addColRefToVariableIfExists',
+  'addColumnAliasLocation',
+  'addColumnLocation',
+  'addCommonTableExpressions',
+  'addCteAliasLocation',
+  'addDatabaseLocation',
+  'addFileLocation',
+  'addFunctionLocation',
+  'addNewDatabaseLocation',
+  'addNewTableLocation',
+  'addStatementLocation',
+  'addStatementTypeLocation',
+  'addSubqueryAliasLocation',
+  'addTableAliasLocation',
+  'addTableLocation',
+  'addTablePrimary',
+  'addUnknownLocation',
+  'addVariableLocation',
+  'applyArgumentTypesToSuggestions',
+  'applyTypeToSuggestions',
+  'checkForKeywords',
+  'checkForSelectListKeywords',
+  'commitLocations',
+  'firstDefined',
+  'getSelectListKeywords',
+  'getSubQuery',
+  'getValueExpressionKeywords',
+  'identifyPartials',
+  'popQueryState',
+  'prepareNewStatement',
+  'pushQueryState',
+  'selectListNoTableSuggest',
+  'suggestAggregateFunctions',
+  'suggestAnalyticFunctions',
+  'suggestColRefKeywords',
+  'suggestColumns',
+  'suggestDatabases',
+  'suggestDdlAndDmlKeywords',
+  'suggestFileFormats',
+  'suggestFilters',
+  'suggestFunctions',
+  'suggestGroupBys',
+  'suggestHdfs',
+  'suggestIdentifiers',
+  'suggestJoinConditions',
+  'suggestJoins',
+  'suggestKeyValues',
+  'suggestKeywords',
+  'suggestOrderBys',
+  'suggestSelectListAliases',
+  'suggestTables',
+  'suggestTablesOrColumns',
+  'suggestValueExpressionKeywords',
+  'suggestValues',
+  'valueExpressionSuggest'
+];
+
+const SYNTAX_PARSER_NOOP = function() {};
+
+const initSyntaxParser = function(parser) {
+  // Noop functions for compatibility with the autocomplete parser as the grammar is shared
+  SYNTAX_PARSER_NOOP_FUNCTIONS.forEach(noopFn => {
+    parser[noopFn] = SYNTAX_PARSER_NOOP;
+  });
+
+  parser.yy.locations = [{}];
+
+  parser.determineCase = function(text) {
+    if (!parser.yy.caseDetermined) {
+      parser.yy.lowerCase = text.toLowerCase() === text;
+      parser.yy.caseDetermined = true;
+    }
+  };
+
+  parser.getKeywordsForOptionalsLR = function() {
+    return [];
+  };
+
+  parser.mergeSuggestKeywords = function() {
+    return {};
+  };
+
+  parser.getTypeKeywords = function() {
+    return [];
+  };
+
+  parser.getColumnDataTypeKeywords = function() {
+    return [];
+  };
+
+  parser.findCaseType = function() {
+    return { types: ['T'] };
+  };
+
+  parser.findReturnTypes = function() {
+    return ['T'];
+  };
+
+  parser.expandImpalaIdentifierChain = function() {
+    return [];
+  };
+
+  parser.expandIdentifierChain = function() {
+    return [];
+  };
+
+  parser.createWeightedKeywords = function() {
+    return [];
+  };
+
+  parser.handleQuotedValueWithCursor = function(lexer, yytext, yylloc, quoteChar) {
+    if (yytext.indexOf('\u2020') !== -1 || yytext.indexOf('\u2021') !== -1) {
+      parser.yy.partialCursor = yytext.indexOf('\u2021') !== -1;
+      const cursorIndex = parser.yy.partialCursor
+        ? yytext.indexOf('\u2021')
+        : yytext.indexOf('\u2020');
+      parser.yy.cursorFound = {
+        first_line: yylloc.first_line,
+        last_line: yylloc.last_line,
+        first_column: yylloc.first_column + cursorIndex,
+        last_column: yylloc.first_column + cursorIndex + 1
+      };
+      const remainder = yytext.substring(cursorIndex + 1);
+      const remainingQuotes = (lexer.upcomingInput().match(new RegExp(quoteChar, 'g')) || [])
+        .length;
+      if (remainingQuotes > 0 && (remainingQuotes & 1) !== 0) {
+        parser.yy.missingEndQuote = false;
+        lexer.input();
+      } else {
+        parser.yy.missingEndQuote = true;
+        lexer.unput(remainder);
+      }
+      lexer.popState();
+      return true;
+    }
+    return false;
+  };
+
+  let lexerModified = false;
+
+  parser.yy.parseError = function(str, hash) {
+    parser.yy.error = hash;
+  };
+
+  const IGNORED_EXPECTED = {
+    ';': true,
+    '.': true,
+    EOF: true,
+    UNSIGNED_INTEGER: true,
+    UNSIGNED_INTEGER_E: true,
+    REGULAR_IDENTIFIER: true,
+    CURSOR: true,
+    PARTIAL_CURSOR: true,
+    HDFS_START_QUOTE: true,
+    HDFS_PATH: true,
+    HDFS_END_QUOTE: true,
+    COMPARISON_OPERATOR: true, // TODO: Expand in results when found
+    ARITHMETIC_OPERATOR: true, // TODO: Expand in results when found
+    VARIABLE_REFERENCE: true,
+    BACKTICK: true,
+    VALUE: true,
+    PARTIAL_VALUE: true,
+    SINGLE_QUOTE: true,
+    DOUBLE_QUOTE: true
+  };
+
+  const CLEAN_EXPECTED = {
+    BETWEEN_AND: 'AND',
+    OVERWRITE_DIRECTORY: 'OVERWRITE',
+    STORED_AS_DIRECTORIES: 'STORED',
+    LIKE_PARQUET: 'LIKE',
+    PARTITION_VALUE: 'PARTITION'
+  };
+
+  parser.parseSyntax = function(beforeCursor, afterCursor, debug) {
+    parser.yy.caseDetermined = false;
+    parser.yy.error = undefined;
+
+    parser.yy.latestTablePrimaries = [];
+    parser.yy.subQueries = [];
+    parser.yy.selectListAliases = [];
+    parser.yy.latestTablePrimaries = [];
+
+    parser.yy.activeDialect = 'impala';
+
+    // TODO: Find a way around throwing an exception when the parser finds a syntax error
+    try {
+      parser.yy.error = false;
+      parser.parse(beforeCursor + afterCursor);
+    } catch (err) {
+      if (debug) {
+        console.log(err);
+        console.error(err.stack);
+        console.log(parser.yy.error);
+      }
+    }
+
+    if (
+      parser.yy.error &&
+      (parser.yy.error.loc.last_column < beforeCursor.length ||
+        !beforeCursor.endsWith(parser.yy.error.text))
+    ) {
+      const weightedExpected = [];
+
+      const addedExpected = {};
+
+      const isLowerCase =
+        (parser.yy.caseDetermined && parser.yy.lowerCase) ||
+        parser.yy.error.text.toLowerCase() === parser.yy.error.text;
+
+      if (
+        parser.yy.error.expected.length === 2 &&
+        parser.yy.error.expected.indexOf("';'") !== -1 &&
+        parser.yy.error.expected.indexOf("'EOF'") !== -1
+      ) {
+        parser.yy.error.expected = [];
+        parser.yy.error.expectedStatementEnd = true;
+        return parser.yy.error;
+      }
+      for (let i = 0; i < parser.yy.error.expected.length; i++) {
+        let expected = parser.yy.error.expected[i];
+        // Strip away the surrounding ' chars
+        expected = expected.substring(1, expected.length - 1);
+        // TODO: Only suggest alphanumeric?
+        if (expected === 'REGULAR_IDENTIFIER') {
+          parser.yy.error.expectedIdentifier = true;
+          if (/^<[a-z]+>/.test(parser.yy.error.token)) {
+            const text = '`' + parser.yy.error.text + '`';
+            weightedExpected.push({
+              text: text,
+              distance: stringDistance(parser.yy.error.text, text, true)
+            });
+            parser.yy.error.possibleReserved = true;
+          }
+        } else if (!IGNORED_EXPECTED[expected] && /[a-z_]+/i.test(expected)) {
+          if (/^<[a-z]+>/.test(expected)) {
+            continue;
+          }
+          expected = CLEAN_EXPECTED[expected] || expected;
+          if (expected === parser.yy.error.text.toUpperCase()) {
+            // Can happen when the lexer entry for a rule contains multiple words like 'stored' in 'stored as parquet'
+            return false;
+          }
+          const text = isLowerCase ? expected.toLowerCase() : expected;
+          if (text && !addedExpected[text]) {
+            addedExpected[text] = true;
+            weightedExpected.push({
+              text: text,
+              distance: stringDistance(parser.yy.error.text, text, true)
+            });
+          }
+        }
+      }
+      if (weightedExpected.length === 0) {
+        parser.yy.error.expected = [];
+        parser.yy.error.incompleteStatement = true;
+        return parser.yy.error;
+      }
+      weightedExpected.sort((a, b) => {
+        if (a.distance === b.distance) {
+          return a.text.localeCompare(b.text);
+        }
+        return a.distance - b.distance;
+      });
+      parser.yy.error.expected = weightedExpected;
+      parser.yy.error.incompleteStatement = true;
+      return parser.yy.error;
+    } else if (parser.yy.error) {
+      parser.yy.error.expected = [];
+      parser.yy.error.incompleteStatement = true;
+      return parser.yy.error;
+    }
+    return false;
+  };
+};
+
+const initGlobalSearchParser = function(parser) {
+  parser.identifyPartials = function(beforeCursor, afterCursor) {
+    const beforeMatch = beforeCursor.match(/[0-9a-zA-Z_]*$/);
+    const afterMatch = afterCursor.match(/^[0-9a-zA-Z_]*(?:\((?:[^)]*\))?)?/);
+    return {
+      left: beforeMatch ? beforeMatch[0].length : 0,
+      right: afterMatch ? afterMatch[0].length : 0
+    };
+  };
+
+  parser.mergeFacets = function(a, b) {
+    if (!a.facets) {
+      a.facets = {};
+    }
+    if (!b.facets) {
+      return;
+    }
+    Object.keys(b.facets).forEach(key => {
+      if (a.facets[key]) {
+        Object.keys(b.facets[key]).forEach(val => {
+          a.facets[key][val.toLowerCase()] = true;
+        });
+      } else {
+        a.facets[key] = b.facets[key];
+      }
+    });
+  };
+
+  parser.mergeText = function(a, b) {
+    if (!a.text) {
+      a.text = [];
+    }
+    if (!b.text) {
+      return;
+    }
+    a.text = a.text.concat(b.text);
+  };
+
+  parser.handleQuotedValueWithCursor = function(lexer, yytext, yylloc, quoteChar) {
+    if (yytext.indexOf('\u2020') !== -1 || yytext.indexOf('\u2021') !== -1) {
+      const cursorIndex = yytext.indexOf('\u2020');
+      parser.yy.cursorFound = {
+        first_line: yylloc.first_line,
+        last_line: yylloc.last_line,
+        first_column: yylloc.first_column + cursorIndex,
+        last_column: yylloc.first_column + cursorIndex + 1
+      };
+      const remainder = yytext.substring(cursorIndex + 1);
+      const remainingQuotes = (lexer.upcomingInput().match(new RegExp(quoteChar, 'g')) || [])
+        .length;
+      if (remainingQuotes > 0 && (remainingQuotes & 1) !== 0) {
+        parser.yy.missingEndQuote = false;
+        lexer.input();
+      } else {
+        parser.yy.missingEndQuote = true;
+        lexer.unput(remainder);
+      }
+      lexer.popState();
+      return true;
+    }
+    return false;
+  };
+
+  parser.parseGlobalSearch = function(beforeCursor, afterCursor, debug) {
+    delete parser.yy.cursorFound;
+
+    let result;
+    try {
+      result = parser.parse(beforeCursor + '\u2020' + afterCursor);
+    } catch (err) {
+      if (debug) {
+        console.log(err);
+        console.error(err.stack);
+        console.log(parser.yy.error);
+      }
+      return {
+        facets: {},
+        text: []
+      };
+    }
+    return result;
+  };
+};
+
+export default {
+  initSqlParser: initSqlParser,
+  initSyntaxParser: initSyntaxParser,
+  stringDistance: stringDistance,
+  initGlobalSearchParser: initGlobalSearchParser
+};

+ 9 - 3
tools/jison/generateParsers.js

@@ -42,12 +42,12 @@ const SQL_STATEMENTS_PARSER_JSDOC =
   ' */\n';
 
 const JISON_FOLDER = 'desktop/core/src/desktop/js/parse/jison/';
-const TARGET_FOLDER = 'desktop/core/src/desktop/js/parse/';
 
 const parserDefinitions = {
   globalSearchParser: {
     sources: ['globalSearchParser.jison'],
     target: 'globalSearchParser.jison',
+    outputFolder: 'desktop/core/src/desktop/js/parse/',
     afterParse: contents =>
       new Promise(resolve => {
         resolve(
@@ -63,6 +63,7 @@ const parserDefinitions = {
   solrFormulaParser: {
     sources: ['solrFormulaParser.jison'],
     target: 'solrFormulaParser.jison',
+    outputFolder: 'desktop/core/src/desktop/js/parse/',
     afterParse: contents =>
       new Promise(resolve => {
         resolve(LICENSE + contents + 'export default solrFormulaParser;\n');
@@ -71,6 +72,7 @@ const parserDefinitions = {
   solrQueryParser: {
     sources: ['solrQueryParser.jison'],
     target: 'solrQueryParser.jison',
+    outputFolder: 'desktop/core/src/desktop/js/parse/',
     afterParse: contents =>
       new Promise(resolve => {
         resolve(LICENSE + contents + 'export default solrQueryParser;\n');
@@ -79,6 +81,7 @@ const parserDefinitions = {
   sqlStatementsParser: {
     sources: ['sqlStatementsParser.jison'],
     target: 'sqlStatementsParser.jison',
+    outputFolder: 'desktop/core/src/desktop/js/parse/',
     afterParse: contents =>
       new Promise(resolve => {
         resolve(
@@ -170,7 +173,7 @@ const generateParser = parserName =>
                 parserConfig
                   .afterParse(contents)
                   .then(finalContents => {
-                    writeFile(TARGET_FOLDER + generatedJsFileName, finalContents)
+                    writeFile(parserConfig.outputFolder + generatedJsFileName, finalContents)
                       .then(() => {
                         deleteFile(generatedJsFileName);
                         console.log('Done!\n');
@@ -208,6 +211,7 @@ const findParser = (fileIndex, folder, sharedFiles, autocomplete) => {
       sources: ['sql/' + folder + '/' + prefix + '_header.jison'].concat(sharedFiles),
       lexer: 'sql/' + folder + '/sql.jisonlex',
       target: 'sql/' + folder + '/' + parserName + '.jison',
+      outputFolder: 'desktop/core/src/desktop/js/parse/sql/' + folder + '/',
       afterParse: contents =>
         new Promise(resolve => {
           resolve(
@@ -215,7 +219,9 @@ const findParser = (fileIndex, folder, sharedFiles, autocomplete) => {
               contents
                 .replace(
                   'var ' + parserName + ' = ',
-                  "import SqlParseSupport from 'parse/sqlParseSupport';\n\nvar " +
+                  "import SqlParseSupport from 'parse/sql/" +
+                    folder +
+                    "/sqlParseSupport';\n\nvar " +
                     parserName +
                     ' = '
                 )

Некоторые файлы не были показаны из-за большого количества измененных файлов