Browse Source

HUE-9370 [editor] Improve the structure of shared parse support utils and extract additional functions

Johan Ahlen 5 years ago
parent
commit
3e950bcdd7

+ 7 - 683
desktop/core/src/desktop/js/parse/sql/calcite/sqlParseSupport.js

@@ -15,54 +15,17 @@
 // limitations under the License.
 
 import { matchesType } from 'sql/reference/typeUtils';
-import stringDistance from 'sql/stringDistance';
 import {
-  applyTypeToSuggestions,
-  getSubQuery,
-  valueExpressionSuggest
+  initSharedAutocomplete,
+  initSyntaxParser,
+  identifierEquals,
+  equalIgnoreCase,
+  SIMPLE_TABLE_REF_SUGGESTIONS
 } from 'parse/sql/sqlParseUtils';
 
-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;
-  };
-}
-
-const equalIgnoreCase = (a, b) => a && b && a.toLowerCase() === b.toLowerCase();
-
-const SIMPLE_TABLE_REF_SUGGESTIONS = [
-  'suggestJoinConditions',
-  'suggestAggregateFunctions',
-  'suggestFilters',
-  'suggestGroupBys',
-  'suggestOrderBys'
-];
-
 const initSqlParser = function(parser) {
+  initSharedAutocomplete(parser);
+
   parser.prepareNewStatement = function() {
     linkTablePrimaries();
     parser.commitLocations();
@@ -292,10 +255,6 @@ const initSqlParser = function(parser) {
     parser.yy.result.suggestJoins = details || {};
   };
 
-  parser.valueExpressionSuggest = valueExpressionSuggest.bind(null, parser);
-
-  parser.applyTypeToSuggestions = applyTypeToSuggestions.bind(null, parser);
-
   parser.findCaseType = function(whenThenList) {
     const types = {};
     whenThenList.caseTypes.forEach(valueExpression => {
@@ -1010,8 +969,6 @@ const initSqlParser = function(parser) {
     }
   };
 
-  parser.getSubQuery = getSubQuery;
-
   parser.addTablePrimary = function(ref) {
     if (typeof parser.yy.latestTablePrimaries === 'undefined') {
       parser.yy.latestTablePrimaries = [];
@@ -1230,80 +1187,6 @@ const initSqlParser = function(parser) {
     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]) {
@@ -1312,295 +1195,6 @@ const initSqlParser = function(parser) {
     }
   };
 
-  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 &&
@@ -1863,276 +1457,6 @@ const initSqlParser = function(parser) {
   };
 };
 
-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.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;
-  };
-
-  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.warn(err);
-        console.warn(err.stack);
-        console.warn(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

+ 27 - 685
desktop/core/src/desktop/js/parse/sql/druid/sqlParseSupport.js

@@ -15,55 +15,17 @@
 // limitations under the License.
 
 import { matchesType } from 'sql/reference/typeUtils';
-import stringDistance from 'sql/stringDistance';
 import {
-  applyTypeToSuggestions,
-  getSubQuery,
-  suggestKeywords,
-  valueExpressionSuggest
+  initSharedAutocomplete,
+  initSyntaxParser,
+  identifierEquals,
+  equalIgnoreCase,
+  SIMPLE_TABLE_REF_SUGGESTIONS
 } from 'parse/sql/sqlParseUtils';
 
-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;
-  };
-}
-
-const equalIgnoreCase = (a, b) => a && b && a.toLowerCase() === b.toLowerCase();
-
-const SIMPLE_TABLE_REF_SUGGESTIONS = [
-  'suggestJoinConditions',
-  'suggestAggregateFunctions',
-  'suggestFilters',
-  'suggestGroupBys',
-  'suggestOrderBys'
-];
-
 const initSqlParser = function(parser) {
+  initSharedAutocomplete(parser);
+
   parser.prepareNewStatement = function() {
     linkTablePrimaries();
     parser.commitLocations();
@@ -293,10 +255,6 @@ const initSqlParser = function(parser) {
     parser.yy.result.suggestJoins = details || {};
   };
 
-  parser.valueExpressionSuggest = valueExpressionSuggest.bind(null, parser);
-
-  parser.applyTypeToSuggestions = applyTypeToSuggestions.bind(null, parser);
-
   parser.findCaseType = function(whenThenList) {
     const types = {};
     whenThenList.caseTypes.forEach(valueExpression => {
@@ -1011,8 +969,6 @@ const initSqlParser = function(parser) {
     }
   };
 
-  parser.getSubQuery = getSubQuery;
-
   parser.addTablePrimary = function(ref) {
     if (typeof parser.yy.latestTablePrimaries === 'undefined') {
       parser.yy.latestTablePrimaries = [];
@@ -1127,7 +1083,26 @@ const initSqlParser = function(parser) {
     return result;
   };
 
-  parser.suggestKeywords = suggestKeywords.bind(null, parser);
+  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;
@@ -1212,80 +1187,6 @@ const initSqlParser = function(parser) {
     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]) {
@@ -1294,295 +1195,6 @@ const initSqlParser = function(parser) {
     }
   };
 
-  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 &&
@@ -1845,276 +1457,6 @@ const initSqlParser = function(parser) {
   };
 };
 
-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.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;
-  };
-
-  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.warn(err);
-        console.warn(err.stack);
-        console.warn(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

+ 27 - 685
desktop/core/src/desktop/js/parse/sql/elasticsearch/sqlParseSupport.js

@@ -15,55 +15,17 @@
 // limitations under the License.
 
 import { matchesType } from 'sql/reference/typeUtils';
-import stringDistance from 'sql/stringDistance';
 import {
-  applyTypeToSuggestions,
-  getSubQuery,
-  suggestKeywords,
-  valueExpressionSuggest
+  initSharedAutocomplete,
+  initSyntaxParser,
+  identifierEquals,
+  equalIgnoreCase,
+  SIMPLE_TABLE_REF_SUGGESTIONS
 } from 'parse/sql/sqlParseUtils';
 
-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;
-  };
-}
-
-const equalIgnoreCase = (a, b) => a && b && a.toLowerCase() === b.toLowerCase();
-
-const SIMPLE_TABLE_REF_SUGGESTIONS = [
-  'suggestJoinConditions',
-  'suggestAggregateFunctions',
-  'suggestFilters',
-  'suggestGroupBys',
-  'suggestOrderBys'
-];
-
 const initSqlParser = function(parser) {
+  initSharedAutocomplete(parser);
+
   parser.prepareNewStatement = function() {
     linkTablePrimaries();
     parser.commitLocations();
@@ -293,10 +255,6 @@ const initSqlParser = function(parser) {
     parser.yy.result.suggestJoins = details || {};
   };
 
-  parser.valueExpressionSuggest = valueExpressionSuggest.bind(null, parser);
-
-  parser.applyTypeToSuggestions = applyTypeToSuggestions.bind(null, parser);
-
   parser.findCaseType = function(whenThenList) {
     const types = {};
     whenThenList.caseTypes.forEach(valueExpression => {
@@ -1011,8 +969,6 @@ const initSqlParser = function(parser) {
     }
   };
 
-  parser.getSubQuery = getSubQuery;
-
   parser.addTablePrimary = function(ref) {
     if (typeof parser.yy.latestTablePrimaries === 'undefined') {
       parser.yy.latestTablePrimaries = [];
@@ -1127,7 +1083,26 @@ const initSqlParser = function(parser) {
     return result;
   };
 
-  parser.suggestKeywords = suggestKeywords.bind(null, parser);
+  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;
@@ -1212,80 +1187,6 @@ const initSqlParser = function(parser) {
     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]) {
@@ -1294,295 +1195,6 @@ const initSqlParser = function(parser) {
     }
   };
 
-  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 &&
@@ -1845,276 +1457,6 @@ const initSqlParser = function(parser) {
   };
 };
 
-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.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;
-  };
-
-  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.warn(err);
-        console.warn(err.stack);
-        console.warn(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

+ 27 - 685
desktop/core/src/desktop/js/parse/sql/flink/sqlParseSupport.js

@@ -15,55 +15,17 @@
 // limitations under the License.
 
 import { matchesType } from 'sql/reference/typeUtils';
-import stringDistance from 'sql/stringDistance';
 import {
-  applyTypeToSuggestions,
-  getSubQuery,
-  suggestKeywords,
-  valueExpressionSuggest
+  initSharedAutocomplete,
+  initSyntaxParser,
+  identifierEquals,
+  equalIgnoreCase,
+  SIMPLE_TABLE_REF_SUGGESTIONS
 } from 'parse/sql/sqlParseUtils';
 
-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;
-  };
-}
-
-const equalIgnoreCase = (a, b) => a && b && a.toLowerCase() === b.toLowerCase();
-
-const SIMPLE_TABLE_REF_SUGGESTIONS = [
-  'suggestJoinConditions',
-  'suggestAggregateFunctions',
-  'suggestFilters',
-  'suggestGroupBys',
-  'suggestOrderBys'
-];
-
 const initSqlParser = function(parser) {
+  initSharedAutocomplete(parser);
+
   parser.prepareNewStatement = function() {
     linkTablePrimaries();
     parser.commitLocations();
@@ -293,10 +255,6 @@ const initSqlParser = function(parser) {
     parser.yy.result.suggestJoins = details || {};
   };
 
-  parser.valueExpressionSuggest = valueExpressionSuggest.bind(null, parser);
-
-  parser.applyTypeToSuggestions = applyTypeToSuggestions.bind(null, parser);
-
   parser.findCaseType = function(whenThenList) {
     const types = {};
     whenThenList.caseTypes.forEach(valueExpression => {
@@ -1011,8 +969,6 @@ const initSqlParser = function(parser) {
     }
   };
 
-  parser.getSubQuery = getSubQuery;
-
   parser.addTablePrimary = function(ref) {
     if (typeof parser.yy.latestTablePrimaries === 'undefined') {
       parser.yy.latestTablePrimaries = [];
@@ -1127,7 +1083,26 @@ const initSqlParser = function(parser) {
     return result;
   };
 
-  parser.suggestKeywords = suggestKeywords.bind(null, parser);
+  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;
@@ -1212,80 +1187,6 @@ const initSqlParser = function(parser) {
     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]) {
@@ -1294,295 +1195,6 @@ const initSqlParser = function(parser) {
     }
   };
 
-  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 &&
@@ -1845,276 +1457,6 @@ const initSqlParser = function(parser) {
   };
 };
 
-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.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;
-  };
-
-  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.warn(err);
-        console.warn(err.stack);
-        console.warn(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

+ 27 - 685
desktop/core/src/desktop/js/parse/sql/generic/sqlParseSupport.js

@@ -15,55 +15,17 @@
 // limitations under the License.
 
 import { matchesType } from 'sql/reference/typeUtils';
-import stringDistance from 'sql/stringDistance';
 import {
-  applyTypeToSuggestions,
-  getSubQuery,
-  suggestKeywords,
-  valueExpressionSuggest
+  initSharedAutocomplete,
+  initSyntaxParser,
+  identifierEquals,
+  equalIgnoreCase,
+  SIMPLE_TABLE_REF_SUGGESTIONS
 } from 'parse/sql/sqlParseUtils';
 
-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;
-  };
-}
-
-const equalIgnoreCase = (a, b) => a && b && a.toLowerCase() === b.toLowerCase();
-
-const SIMPLE_TABLE_REF_SUGGESTIONS = [
-  'suggestJoinConditions',
-  'suggestAggregateFunctions',
-  'suggestFilters',
-  'suggestGroupBys',
-  'suggestOrderBys'
-];
-
 const initSqlParser = function(parser) {
+  initSharedAutocomplete(parser);
+
   parser.prepareNewStatement = function() {
     linkTablePrimaries();
     parser.commitLocations();
@@ -293,10 +255,6 @@ const initSqlParser = function(parser) {
     parser.yy.result.suggestJoins = details || {};
   };
 
-  parser.valueExpressionSuggest = valueExpressionSuggest.bind(null, parser);
-
-  parser.applyTypeToSuggestions = applyTypeToSuggestions.bind(null, parser);
-
   parser.findCaseType = function(whenThenList) {
     const types = {};
     whenThenList.caseTypes.forEach(valueExpression => {
@@ -1011,8 +969,6 @@ const initSqlParser = function(parser) {
     }
   };
 
-  parser.getSubQuery = getSubQuery;
-
   parser.addTablePrimary = function(ref) {
     if (typeof parser.yy.latestTablePrimaries === 'undefined') {
       parser.yy.latestTablePrimaries = [];
@@ -1127,7 +1083,26 @@ const initSqlParser = function(parser) {
     return result;
   };
 
-  parser.suggestKeywords = suggestKeywords.bind(null, parser);
+  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;
@@ -1212,80 +1187,6 @@ const initSqlParser = function(parser) {
     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]) {
@@ -1294,295 +1195,6 @@ const initSqlParser = function(parser) {
     }
   };
 
-  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 &&
@@ -1845,276 +1457,6 @@ const initSqlParser = function(parser) {
   };
 };
 
-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.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;
-  };
-
-  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.warn(err);
-        console.warn(err.stack);
-        console.warn(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

+ 10 - 782
desktop/core/src/desktop/js/parse/sql/hive/sqlParseSupport.js

@@ -16,50 +16,17 @@
 
 import KEYWORDS from './keywords.json';
 import { matchesType } from 'sql/reference/typeUtils';
-import stringDistance from 'sql/stringDistance';
-import { applyTypeToSuggestions, getSubQuery, suggestKeywords } from 'parse/sql/sqlParseUtils';
-
-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;
-  };
-}
-
-const equalIgnoreCase = (a, b) => a && b && a.toLowerCase() === b.toLowerCase();
-
-const SIMPLE_TABLE_REF_SUGGESTIONS = [
-  'suggestJoinConditions',
-  'suggestAggregateFunctions',
-  'suggestFilters',
-  'suggestGroupBys',
-  'suggestOrderBys'
-];
+import {
+  initSharedAutocomplete,
+  initSyntaxParser,
+  identifierEquals,
+  equalIgnoreCase,
+  SIMPLE_TABLE_REF_SUGGESTIONS
+} from 'parse/sql/sqlParseUtils';
 
 const initSqlParser = function(parser) {
+  initSharedAutocomplete(parser);
+
   parser.prepareNewStatement = function() {
     linkTablePrimaries();
     parser.commitLocations();
@@ -317,8 +284,6 @@ const initSqlParser = function(parser) {
     parser.suggestKeywords(keywords);
   };
 
-  parser.applyTypeToSuggestions = applyTypeToSuggestions.bind(null, parser);
-
   parser.findCaseType = function(whenThenList) {
     const types = {};
     whenThenList.caseTypes.forEach(valueExpression => {
@@ -1155,8 +1120,6 @@ const initSqlParser = function(parser) {
     }
   };
 
-  parser.getSubQuery = getSubQuery;
-
   parser.addTablePrimary = function(ref) {
     if (typeof parser.yy.latestTablePrimaries === 'undefined') {
       parser.yy.latestTablePrimaries = [];
@@ -1294,8 +1257,6 @@ const initSqlParser = function(parser) {
 
   parser.KEYWORDS = KEYWORDS;
 
-  parser.suggestKeywords = suggestKeywords.bind(null, parser);
-
   parser.suggestColRefKeywords = function(colRefKeywords) {
     parser.yy.result.suggestColRefKeywords = colRefKeywords;
   };
@@ -1379,80 +1340,6 @@ const initSqlParser = function(parser) {
     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]) {
@@ -1461,295 +1348,6 @@ const initSqlParser = function(parser) {
     }
   };
 
-  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 &&
@@ -2013,377 +1611,7 @@ const initSqlParser = function(parser) {
   };
 };
 
-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.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';
-
-    // Fix for parser bug when switching lexer states
-    if (!lexerModified) {
-      const originalSetInput = parser.lexer.setInput;
-      parser.lexer.setInput = function(input, yy) {
-        return originalSetInput.bind(parser.lexer)(input, yy);
-      };
-      lexerModified = true;
-    }
-
-    // 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.warn(err);
-        console.warn(err.stack);
-        console.warn(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.warn(err);
-        console.warn(err.stack);
-        console.warn(parser.yy.error);
-      }
-      return {
-        facets: {},
-        text: []
-      };
-    }
-    return result;
-  };
-};
-
 export default {
   initSqlParser: initSqlParser,
-  initSyntaxParser: initSyntaxParser,
-  initGlobalSearchParser: initGlobalSearchParser
+  initSyntaxParser: initSyntaxParser
 };

+ 11 - 783
desktop/core/src/desktop/js/parse/sql/impala/sqlParseSupport.js

@@ -15,50 +15,17 @@
 // limitations under the License.
 
 import { matchesType } from 'sql/reference/typeUtils';
-import stringDistance from 'sql/stringDistance';
-import { applyTypeToSuggestions, getSubQuery, suggestKeywords } from 'parse/sql/sqlParseUtils';
-
-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;
-  };
-}
-
-const equalIgnoreCase = (a, b) => a && b && a.toLowerCase() === b.toLowerCase();
-
-const SIMPLE_TABLE_REF_SUGGESTIONS = [
-  'suggestJoinConditions',
-  'suggestAggregateFunctions',
-  'suggestFilters',
-  'suggestGroupBys',
-  'suggestOrderBys'
-];
+import {
+  initSyntaxParser,
+  initSharedAutocomplete,
+  identifierEquals,
+  equalIgnoreCase,
+  SIMPLE_TABLE_REF_SUGGESTIONS
+} from 'parse/sql/sqlParseUtils';
 
 const initSqlParser = function(parser) {
+  initSharedAutocomplete(parser);
+
   parser.prepareNewStatement = function() {
     linkTablePrimaries();
     parser.commitLocations();
@@ -314,15 +281,13 @@ const initSqlParser = function(parser) {
       keywords = keywords.concat(['EXISTS', 'NOT']);
     }
     if (oppositeValueExpression && oppositeValueExpression.types[0] === 'NUMBER') {
-      applyTypeToSuggestions(parser, oppositeValueExpression);
+      parser.applyTypeToSuggestions(oppositeValueExpression);
     } else if (typeof operator === 'undefined' || operator === '-' || operator === '+') {
       keywords.push('INTERVAL');
     }
     parser.suggestKeywords(keywords);
   };
 
-  parser.applyTypeToSuggestions = applyTypeToSuggestions.bind(null, parser);
-
   parser.findCaseType = function(whenThenList) {
     const types = {};
     whenThenList.caseTypes.forEach(valueExpression => {
@@ -1241,8 +1206,6 @@ const initSqlParser = function(parser) {
     }
   };
 
-  parser.getSubQuery = getSubQuery;
-
   parser.addTablePrimary = function(ref) {
     if (typeof parser.yy.latestTablePrimaries === 'undefined') {
       parser.yy.latestTablePrimaries = [];
@@ -1367,8 +1330,6 @@ const initSqlParser = function(parser) {
     return result;
   };
 
-  parser.suggestKeywords = suggestKeywords.bind(null, parser);
-
   parser.suggestColRefKeywords = function(colRefKeywords) {
     parser.yy.result.suggestColRefKeywords = colRefKeywords;
   };
@@ -1452,80 +1413,6 @@ const initSqlParser = function(parser) {
     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]) {
@@ -1534,295 +1421,6 @@ const initSqlParser = function(parser) {
     }
   };
 
-  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 &&
@@ -2085,377 +1683,7 @@ const initSqlParser = function(parser) {
   };
 };
 
-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.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';
-
-    // Fix for parser bug when switching lexer states
-    if (!lexerModified) {
-      const originalSetInput = parser.lexer.setInput;
-      parser.lexer.setInput = function(input, yy) {
-        return originalSetInput.bind(parser.lexer)(input, yy);
-      };
-      lexerModified = true;
-    }
-
-    // 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.warn(err);
-        console.warn(err.stack);
-        console.warn(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.warn(err);
-        console.warn(err.stack);
-        console.warn(parser.yy.error);
-      }
-      return {
-        facets: {},
-        text: []
-      };
-    }
-    return result;
-  };
-};
-
 export default {
   initSqlParser: initSqlParser,
-  initSyntaxParser: initSyntaxParser,
-  initGlobalSearchParser: initGlobalSearchParser
+  initSyntaxParser: initSyntaxParser
 };

+ 7 - 679
desktop/core/src/desktop/js/parse/sql/ksql/sqlParseSupport.js

@@ -15,55 +15,17 @@
 // limitations under the License.
 
 import { matchesType } from 'sql/reference/typeUtils';
-import stringDistance from 'sql/stringDistance';
 import {
-  applyTypeToSuggestions,
-  getSubQuery,
-  suggestKeywords,
-  valueExpressionSuggest
+  initSharedAutocomplete,
+  initSyntaxParser,
+  identifierEquals,
+  equalIgnoreCase,
+  SIMPLE_TABLE_REF_SUGGESTIONS
 } from 'parse/sql/sqlParseUtils';
 
-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;
-  };
-}
-
-const equalIgnoreCase = (a, b) => a && b && a.toLowerCase() === b.toLowerCase();
-
-const SIMPLE_TABLE_REF_SUGGESTIONS = [
-  'suggestJoinConditions',
-  'suggestAggregateFunctions',
-  'suggestFilters',
-  'suggestGroupBys',
-  'suggestOrderBys'
-];
-
 const initSqlParser = function(parser) {
+  initSharedAutocomplete(parser);
+
   parser.prepareNewStatement = function() {
     linkTablePrimaries();
     parser.commitLocations();
@@ -293,10 +255,6 @@ const initSqlParser = function(parser) {
     parser.yy.result.suggestJoins = details || {};
   };
 
-  parser.valueExpressionSuggest = valueExpressionSuggest.bind(null, parser);
-
-  parser.applyTypeToSuggestions = applyTypeToSuggestions.bind(null, parser);
-
   parser.findCaseType = function(whenThenList) {
     const types = {};
     whenThenList.caseTypes.forEach(valueExpression => {
@@ -1011,8 +969,6 @@ const initSqlParser = function(parser) {
     }
   };
 
-  parser.getSubQuery = getSubQuery;
-
   parser.addTablePrimary = function(ref) {
     if (typeof parser.yy.latestTablePrimaries === 'undefined') {
       parser.yy.latestTablePrimaries = [];
@@ -1122,8 +1078,6 @@ const initSqlParser = function(parser) {
     return result;
   };
 
-  parser.suggestKeywords = suggestKeywords.bind(null, parser);
-
   parser.suggestColRefKeywords = function(colRefKeywords) {
     parser.yy.result.suggestColRefKeywords = colRefKeywords;
   };
@@ -1207,80 +1161,6 @@ const initSqlParser = function(parser) {
     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]) {
@@ -1289,288 +1169,6 @@ const initSqlParser = function(parser) {
     }
   };
 
-  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+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+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;
-        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 &&
@@ -1833,276 +1431,6 @@ const initSqlParser = function(parser) {
   };
 };
 
-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.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;
-  };
-
-  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.warn(err);
-        console.warn(err.stack);
-        console.warn(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

+ 7 - 686
desktop/core/src/desktop/js/parse/sql/phoenix/sqlParseSupport.js

@@ -15,55 +15,17 @@
 // limitations under the License.
 
 import { matchesType } from 'sql/reference/typeUtils';
-import stringDistance from 'sql/stringDistance';
 import {
-  applyTypeToSuggestions,
-  getSubQuery,
-  suggestKeywords,
-  valueExpressionSuggest
+  initSharedAutocomplete,
+  initSyntaxParser,
+  identifierEquals,
+  equalIgnoreCase,
+  SIMPLE_TABLE_REF_SUGGESTIONS
 } from 'parse/sql/sqlParseUtils';
 
-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;
-  };
-}
-
-const equalIgnoreCase = (a, b) => a && b && a.toLowerCase() === b.toLowerCase();
-
-const SIMPLE_TABLE_REF_SUGGESTIONS = [
-  'suggestJoinConditions',
-  'suggestAggregateFunctions',
-  'suggestFilters',
-  'suggestGroupBys',
-  'suggestOrderBys'
-];
-
 const initSqlParser = function(parser) {
+  initSharedAutocomplete(parser);
+
   parser.prepareNewStatement = function() {
     linkTablePrimaries();
     parser.commitLocations();
@@ -293,10 +255,6 @@ const initSqlParser = function(parser) {
     parser.yy.result.suggestJoins = details || {};
   };
 
-  parser.valueExpressionSuggest = valueExpressionSuggest.bind(null, parser);
-
-  parser.applyTypeToSuggestions = applyTypeToSuggestions.bind(null, parser);
-
   parser.findCaseType = function(whenThenList) {
     const types = {};
     whenThenList.caseTypes.forEach(valueExpression => {
@@ -1011,8 +969,6 @@ const initSqlParser = function(parser) {
     }
   };
 
-  parser.getSubQuery = getSubQuery;
-
   parser.addTablePrimary = function(ref) {
     if (typeof parser.yy.latestTablePrimaries === 'undefined') {
       parser.yy.latestTablePrimaries = [];
@@ -1127,8 +1083,6 @@ const initSqlParser = function(parser) {
     return result;
   };
 
-  parser.suggestKeywords = suggestKeywords.bind(null, parser);
-
   parser.suggestColRefKeywords = function(colRefKeywords) {
     parser.yy.result.suggestColRefKeywords = colRefKeywords;
   };
@@ -1212,80 +1166,6 @@ const initSqlParser = function(parser) {
     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]) {
@@ -1294,295 +1174,6 @@ const initSqlParser = function(parser) {
     }
   };
 
-  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 &&
@@ -1845,276 +1436,6 @@ const initSqlParser = function(parser) {
   };
 };
 
-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.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;
-  };
-
-  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.warn(err);
-        console.warn(err.stack);
-        console.warn(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

+ 10 - 841
desktop/core/src/desktop/js/parse/sql/presto/sqlParseSupport.js

@@ -15,109 +15,17 @@
 // limitations under the License.
 
 import { matchesType } from 'sql/reference/typeUtils';
-import { applyTypeToSuggestions, getSubQuery, suggestKeywords } from 'parse/sql/sqlParseUtils';
-
-/**
- * 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 = (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 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;
-  };
-}
-
-const equalIgnoreCase = (a, b) => a && b && a.toLowerCase() === b.toLowerCase();
-
-const SIMPLE_TABLE_REF_SUGGESTIONS = [
-  'suggestJoinConditions',
-  'suggestAggregateFunctions',
-  'suggestFilters',
-  'suggestGroupBys',
-  'suggestOrderBys'
-];
+import {
+  initSharedAutocomplete,
+  initSyntaxParser,
+  identifierEquals,
+  equalIgnoreCase,
+  SIMPLE_TABLE_REF_SUGGESTIONS
+} from 'parse/sql/sqlParseUtils';
 
 const initSqlParser = function(parser) {
+  initSharedAutocomplete(parser);
+
   parser.prepareNewStatement = function() {
     linkTablePrimaries();
     parser.commitLocations();
@@ -374,8 +282,6 @@ const initSqlParser = function(parser) {
     parser.suggestKeywords(keywords);
   };
 
-  parser.applyTypeToSuggestions = applyTypeToSuggestions.bind(null, parser);
-
   parser.findCaseType = function(whenThenList) {
     const types = {};
     whenThenList.caseTypes.forEach(valueExpression => {
@@ -1212,8 +1118,6 @@ const initSqlParser = function(parser) {
     }
   };
 
-  parser.getSubQuery = getSubQuery;
-
   parser.addTablePrimary = function(ref) {
     if (typeof parser.yy.latestTablePrimaries === 'undefined') {
       parser.yy.latestTablePrimaries = [];
@@ -1342,8 +1246,6 @@ const initSqlParser = function(parser) {
     return result;
   };
 
-  parser.suggestKeywords = suggestKeywords.bind(null, parser);
-
   parser.suggestColRefKeywords = function(colRefKeywords) {
     parser.yy.result.suggestColRefKeywords = colRefKeywords;
   };
@@ -1427,80 +1329,6 @@ const initSqlParser = function(parser) {
     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]) {
@@ -1509,295 +1337,6 @@ const initSqlParser = function(parser) {
     }
   };
 
-  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 &&
@@ -2061,377 +1600,7 @@ const initSqlParser = function(parser) {
   };
 };
 
-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.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';
-
-    // Fix for parser bug when switching lexer states
-    if (!lexerModified) {
-      const originalSetInput = parser.lexer.setInput;
-      parser.lexer.setInput = function(input, yy) {
-        return originalSetInput.bind(parser.lexer)(input, yy);
-      };
-      lexerModified = true;
-    }
-
-    // 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.warn(err);
-        console.warn(err.stack);
-        console.warn(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.warn(err);
-        console.warn(err.stack);
-        console.warn(parser.yy.error);
-      }
-      return {
-        facets: {},
-        text: []
-      };
-    }
-    return result;
-  };
-};
-
 export default {
   initSqlParser: initSqlParser,
-  initSyntaxParser: initSyntaxParser,
-  initGlobalSearchParser: initGlobalSearchParser
+  initSyntaxParser: initSyntaxParser
 };

+ 751 - 88
desktop/core/src/desktop/js/parse/sql/sqlParseUtils.js

@@ -14,110 +14,773 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-export const getSubQuery = 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 }];
+import stringDistance from 'sql/stringDistance';
+
+// 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;
+  };
+}
+
+export const identifierEquals = (a, b) =>
+  a &&
+  b &&
+  a
+    .replace(/^\s*`/, '')
+    .replace(/`\s*$/, '')
+    .toLowerCase() ===
+    b
+      .replace(/^\s*`/, '')
+      .replace(/`\s*$/, '')
+      .toLowerCase();
+
+export const equalIgnoreCase = (a, b) => a && b && a.toLowerCase() === b.toLowerCase();
+
+export const SIMPLE_TABLE_REF_SUGGESTIONS = [
+  'suggestJoinConditions',
+  'suggestAggregateFunctions',
+  'suggestFilters',
+  'suggestGroupBys',
+  'suggestOrderBys'
+];
+
+export const initSharedAutocomplete = parser => {
+  const adjustLocationForCursor = 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.addAsteriskLocation = (location, identifierChain) => {
+    parser.yy.locations.push({
+      type: 'asterisk',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addClauseLocation = (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.addColumnAliasLocation = (location, alias, parentLocation) => {
+    const aliasLocation = {
+      type: 'alias',
+      source: 'column',
+      alias: alias,
+      location: adjustLocationForCursor(location),
+      parentLocation: adjustLocationForCursor(parentLocation)
+    };
     if (
-      col.valueExpression &&
-      col.valueExpression.types &&
-      col.valueExpression.types.length === 1
+      parser.yy.locations.length &&
+      parser.yy.locations[parser.yy.locations.length - 1].type === 'column'
     ) {
-      result.type = col.valueExpression.types[0];
-      if (result.type === 'UDFREF') {
-        if (col.valueExpression.function) {
-          result.udfRef = col.valueExpression.function;
-        } else {
-          result.type = ['T'];
-        }
+      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);
+  };
 
-    columns.push(result);
-  });
+  parser.addColumnLocation = (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
+      });
+    }
+  };
 
-  return {
-    columns: columns
+  parser.addCteAliasLocation = (location, alias) => {
+    parser.yy.locations.push({
+      type: 'alias',
+      source: 'cte',
+      alias: alias,
+      location: adjustLocationForCursor(location)
+    });
+  };
+
+  parser.addDatabaseLocation = (location, identifierChain) => {
+    parser.yy.locations.push({
+      type: 'database',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addFileLocation = (location, path) => {
+    parser.yy.locations.push({
+      type: 'file',
+      location: adjustLocationForCursor(location),
+      path: path
+    });
+  };
+
+  parser.addFunctionLocation = (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.addNewDatabaseLocation = (location, identifierChain) => {
+    parser.yy.definitions.push({
+      type: 'database',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
   };
-};
 
-const applyTypes = (suggestion, typeDetails) => {
-  suggestion.types = typeDetails.types;
-  if (typeDetails.types && typeDetails.types[0] === 'UDFREF') {
-    if (typeDetails.function) {
-      suggestion.udfRef = typeDetails.function;
+  parser.addNewTableLocation = (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.addStatementLocation = 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 {
-      suggestion.types = ['T'];
+      adjustedLocation = {
+        first_line: location.first_line,
+        last_line: location.last_line,
+        first_column: location.first_column + 1,
+        last_column: location.last_column + 1
+      };
     }
-  }
-};
 
-export const applyTypeToSuggestions = (parser, details) => {
-  if (!details.types) {
-    console.trace();
-  }
-  if (details.types[0] === 'BOOLEAN') {
-    return;
-  }
-  if (parser.yy.result.suggestFunctions && !parser.yy.result.suggestFunctions.types) {
-    applyTypes(parser.yy.result.suggestFunctions, details);
-  }
-  if (parser.yy.result.suggestColumns && !parser.yy.result.suggestColumns.types) {
-    applyTypes(parser.yy.result.suggestColumns, details);
-  }
-};
+    parser.yy.locations.push({
+      type: 'statement',
+      location: adjustedLocation
+    });
+  };
 
-export const valueExpressionSuggest = (parser, 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') {
-    applyTypeToSuggestions(parser, oppositeValueExpression);
-  }
-  parser.suggestKeywords(keywords);
-};
+  parser.addStatementTypeLocation = (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);
+  };
 
-export const suggestKeywords = (parser, keywords) => {
-  if (typeof keywords === 'string') {
-    keywords = (parser.KEYWORDS && parser.KEYWORDS[keywords]) || [];
-  }
-
-  const weightedKeywords = [];
-  if (keywords.length === 0) {
-    return;
-  }
-  keywords.forEach(keyword => {
-    if (typeof keyword.weight !== 'undefined') {
-      weightedKeywords.push(keyword);
+  parser.addSubqueryAliasLocation = (location, alias) => {
+    parser.yy.locations.push({
+      type: 'alias',
+      source: 'subquery',
+      alias: alias,
+      location: adjustLocationForCursor(location)
+    });
+  };
+
+  parser.addTableAliasLocation = (location, alias, identifierChain) => {
+    parser.yy.locations.push({
+      type: 'alias',
+      source: 'table',
+      alias: alias,
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addTableLocation = (location, identifierChain) => {
+    parser.yy.locations.push({
+      type: 'table',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addVariableLocation = (location, value) => {
+    if (/\${[^}]*}/.test(value)) {
+      parser.yy.locations.push({
+        type: 'variable',
+        location: adjustLocationForCursor(location),
+        value: value
+      });
+    }
+  };
+
+  parser.addUnknownLocation = (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 {
-      weightedKeywords.push({ value: keyword, weight: -1 });
+      loc = {
+        type: 'unknown',
+        location: adjustLocationForCursor(location),
+        identifierChain: identifierChain,
+        qualified: identifierChain.length > 1
+      };
     }
-  });
-  weightedKeywords.sort((a, b) => {
-    if (a.weight !== b.weight) {
-      return b.weight - a.weight;
+    parser.yy.locations.push(loc);
+    return loc;
+  };
+
+  parser.applyTypes = (suggestion, typeDetails) => {
+    suggestion.types = typeDetails.types;
+    if (typeDetails.types && typeDetails.types[0] === 'UDFREF') {
+      if (typeDetails.function) {
+        suggestion.udfRef = typeDetails.function;
+      } else {
+        suggestion.types = ['T'];
+      }
+    }
+  };
+
+  parser.applyTypeToSuggestions = details => {
+    if (!details.types) {
+      console.trace();
+    }
+    if (details.types[0] === 'BOOLEAN') {
+      return;
+    }
+    if (parser.yy.result.suggestFunctions && !parser.yy.result.suggestFunctions.types) {
+      parser.applyTypes(parser.yy.result.suggestFunctions, details);
+    }
+    if (parser.yy.result.suggestColumns && !parser.yy.result.suggestColumns.types) {
+      parser.applyTypes(parser.yy.result.suggestColumns, details);
+    }
+  };
+
+  parser.getSubQuery = 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];
+        if (result.type === 'UDFREF') {
+          if (col.valueExpression.function) {
+            result.udfRef = col.valueExpression.function;
+          } else {
+            result.type = ['T'];
+          }
+        }
+      }
+
+      columns.push(result);
+    });
+
+    return {
+      columns: columns
+    };
+  };
+
+  parser.suggestKeywords = keywords => {
+    if (typeof keywords === 'string') {
+      keywords = (parser.KEYWORDS && parser.KEYWORDS[keywords]) || [];
+    }
+
+    const weightedKeywords = [];
+    if (keywords.length === 0) {
+      return;
     }
-    return a.value.localeCompare(b.value);
+    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.valueExpressionSuggest = (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(oppositeValueExpression);
+    }
+    parser.suggestKeywords(keywords);
+  };
+};
+
+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() {};
+
+export const initSyntaxParser = 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.result.suggestKeywords = weightedKeywords;
+
+  parser.yy.locations = [{}];
+
+  parser.determineCase = text => {
+    if (!parser.yy.caseDetermined) {
+      parser.yy.lowerCase = text.toLowerCase() === text;
+      parser.yy.caseDetermined = true;
+    }
+  };
+
+  parser.getKeywordsForOptionalsLR = () => [];
+
+  parser.mergeSuggestKeywords = () => ({});
+
+  parser.getTypeKeywords = () => [];
+
+  parser.getColumnDataTypeKeywords = () => [];
+
+  parser.findCaseType = () => ({ types: ['T'] });
+
+  parser.expandIdentifierChain = () => [];
+
+  parser.createWeightedKeywords = () => [];
+
+  parser.handleQuotedValueWithCursor = (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;
+  };
+
+  parser.yy.parseError = (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 = (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.warn(err);
+        console.warn(err.stack);
+        console.warn(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;
+  };
 };