text: // Licensed to Cloudera, Inc. under one
  // or more contributor license agreements.  See the NOTICE file
  // distributed with this work for additional information
  // regarding copyright ownership.  Cloudera, Inc. licenses this file
  // to you under the Apache License, Version 2.0 (the
  // "License"); you may not use this file except in compliance
  // with the License.  You may obtain a copy of the License at
  //
  //     http://www.apache.org/licenses/LICENSE-2.0
  //
  // Unless required by applicable law or agreed to in writing, software
  // distributed under the License is distributed on an "AS IS" BASIS,
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  // See the License for the specific language governing permissions and
  // limitations under the License.
  
  var SqlParseSupport = (function () {
  
    var equalIgnoreCase = function (a, b) {
      return a && b && a.toLowerCase() === b.toLowerCase();
    };
  
    var initSqlParser = function (parser) {
  
      var SIMPLE_TABLE_REF_SUGGESTIONS = ['suggestJoinConditions', 'suggestAggregateFunctions', 'suggestFilters', 'suggestGroupBys', 'suggestOrderBys'];
  
      parser.prepareNewStatement = function () {
        linkTablePrimaries();
        parser.commitLocations();
  
        delete parser.yy.lateralViews;
        delete parser.yy.latestCommonTableExpressions;
        delete parser.yy.correlatedSubQuery;
        parser.yy.subQueries = [];
        parser.yy.selectListAliases = [];
        parser.yy.latestTablePrimaries = [];
  
        parser.parseError = function (message, error) {
          parser.yy.errors.push(error);
          return message;
        };
        prioritizeSuggestions();
      };
  
      parser.addCommonTableExpressions = function (identifiers) {
        parser.yy.result.commonTableExpressions = identifiers;
        parser.yy.latestCommonTableExpressions = identifiers;
      };
  
      parser.pushQueryState = function () {
        parser.yy.resultStack.push(parser.yy.result);
        parser.yy.locationsStack.push(parser.yy.locations);
        parser.yy.lateralViewsStack.push(parser.yy.lateralViews);
        parser.yy.selectListAliasesStack.push(parser.yy.selectListAliases);
        parser.yy.primariesStack.push(parser.yy.latestTablePrimaries);
        parser.yy.subQueriesStack.push(parser.yy.subQueries);
  
        parser.yy.result = {};
        parser.yy.locations = [];
        parser.yy.selectListAliases = []; // Not allowed in correlated sub-queries
        parser.yy.lateralViews = []; // Not allowed in correlated sub-queries
  
        if (parser.yy.correlatedSubQuery) {
          parser.yy.latestTablePrimaries = parser.yy.latestTablePrimaries.concat();
          parser.yy.subQueries = parser.yy.subQueries.concat();
        } else {
          parser.yy.latestTablePrimaries = [];
          parser.yy.subQueries = [];
        }
      };
  
      parser.popQueryState = function (subQuery) {
        linkTablePrimaries();
        parser.commitLocations();
  
        if (Object.keys(parser.yy.result).length === 0) {
          parser.yy.result = parser.yy.resultStack.pop();
        } else {
          parser.yy.resultStack.pop();
        }
        var oldSubQueries = parser.yy.subQueries;
        parser.yy.subQueries = parser.yy.subQueriesStack.pop();
        if (subQuery) {
          if (oldSubQueries.length > 0) {
            subQuery.subQueries = oldSubQueries;
          }
          parser.yy.subQueries.push(subQuery);
        }
  
        parser.yy.lateralViews = parser.yy.lateralViewsStack.pop();
        parser.yy.latestTablePrimaries = parser.yy.primariesStack.pop();
        parser.yy.locations = parser.yy.locationsStack.pop();
        parser.yy.selectListAliases = parser.yy.selectListAliasesStack.pop();
      };
  
      parser.suggestSelectListAliases = function () {
        if (parser.yy.selectListAliases && parser.yy.selectListAliases.length > 0 && parser.yy.result.suggestColumns
          && (typeof parser.yy.result.suggestColumns.identifierChain === 'undefined' || parser.yy.result.suggestColumns.identifierChain.length === 0)) {
          parser.yy.result.suggestColumnAliases = parser.yy.selectListAliases;
        }
      };
  
      parser.isHive = function () {
        return parser.yy.activeDialect === 'hive';
      };
  
      parser.isImpala = function () {
        return parser.yy.activeDialect === 'impala';
      };
  
      parser.mergeSuggestKeywords = function () {
        var result = [];
        Array.prototype.slice.call(arguments).forEach(function (suggestion) {
          if (typeof suggestion !== 'undefined' && typeof suggestion.suggestKeywords !== 'undefined') {
            result = result.concat(suggestion.suggestKeywords);
          }
        });
        if (result.length > 0) {
          return {suggestKeywords: result};
        }
        return {};
      };
  
      parser.suggestValueExpressionKeywords = function (valueExpression, extras) {
        var expressionKeywords = parser.getValueExpressionKeywords(valueExpression, extras);
        parser.suggestKeywords(expressionKeywords.suggestKeywords);
        if (expressionKeywords.suggestColRefKeywords) {
          parser.suggestColRefKeywords(expressionKeywords.suggestColRefKeywords);
        }
        if (valueExpression.lastType) {
          parser.addColRefIfExists(valueExpression.lastType);
        } else {
          parser.addColRefIfExists(valueExpression);
        }
      };
  
      parser.getValueExpressionKeywords = function (valueExpression, extras) {
        var types = valueExpression.lastType ? valueExpression.lastType.types : valueExpression.types;
        // We could have valueExpression.columnReference to suggest based on column type
        var keywords = ['<', '<=', '<>', '=', '>', '>=', 'BETWEEN', 'IN', 'IS NOT NULL', 'IS NULL', 'NOT BETWEEN', 'NOT IN'];
        if (parser.isHive()) {
          keywords.push('<=>');
        }
        if (extras) {
          keywords = keywords.concat(extras);
        }
        if (valueExpression.suggestKeywords) {
          keywords = keywords.concat(valueExpression.suggestKeywords);
        }
        if (types.length === 1 && types[0] === 'COLREF') {
          return {
            suggestKeywords: keywords,
            suggestColRefKeywords: {
              BOOLEAN: ['AND', 'OR'],
              NUMBER: ['+', '-', '*', '/', '%'],
              STRING: ['LIKE', 'NOT LIKE', 'REGEX', 'RLIKE']
            }
          }
        }
        if (SqlFunctions.matchesType(parser.yy.activeDialect, ['BOOLEAN'], types)) {
          keywords = keywords.concat(['AND', 'OR']);
        }
        if (SqlFunctions.matchesType(parser.yy.activeDialect, ['NUMBER'], types)) {
          keywords = keywords.concat(['+', '-', '*', '/', '%']);
        }
        if (SqlFunctions.matchesType(parser.yy.activeDialect, ['STRING'], types)) {
          keywords = keywords.concat(['LIKE', 'NOT LIKE', 'REGEX', 'RLIKE']);
        }
        return {suggestKeywords: keywords};
      };
  
      parser.getTypeKeywords = function () {
        if (parser.isHive()) {
          return ['BIGINT', 'BINARY', 'BOOLEAN', 'CHAR', 'DATE', 'DECIMAL', 'DOUBLE', 'FLOAT', 'INT', 'SMALLINT', 'TIMESTAMP', 'STRING', 'TINYINT', 'VARCHAR'];
        }
        if (parser.isImpala()) {
          return ['BIGINT', 'BOOLEAN', 'CHAR', 'DECIMAL', 'DOUBLE', 'FLOAT', 'INT', 'REAL', 'SMALLINT', 'TIMESTAMP', 'STRING', 'TINYINT', 'VARCHAR'];
        }
        return ['BIGINT', 'BOOLEAN', 'CHAR', 'DECIMAL', 'DOUBLE', 'FLOAT', 'INT', 'SMALLINT', 'TIMESTAMP', 'STRING', 'TINYINT', 'VARCHAR'];
      };
  
      parser.getColumnDataTypeKeywords = function () {
        if (parser.isHive()) {
          return parser.getTypeKeywords().concat(['ARRAY<>', 'MAP<>', 'STRUCT<>', 'UNIONTYPE<>']);
        }
        return parser.getTypeKeywords();
      };
  
      parser.addColRefIfExists = function (valueExpression) {
        if (valueExpression.columnReference) {
          parser.yy.result.colRef = {identifierChain: valueExpression.columnReference};
        }
      };
  
      parser.selectListNoTableSuggest = function (selectListEdit, hasDistinctOrAll) {
        if (selectListEdit.cursorAtStart) {
          var keywords = [];
          if (hasDistinctOrAll) {
            keywords = [{value: '*', weight: 1000}];
          } else {
            keywords = [{value: '*', weight: 1000}, 'ALL', 'DISTINCT'];
          }
          if (parser.isImpala()) {
            keywords.push('STRAIGHT_JOIN');
          }
          parser.suggestKeywords(keywords);
        } else {
          parser.checkForKeywords(selectListEdit);
        }
        if (selectListEdit.suggestFunctions) {
          parser.suggestFunctions();
        }
        if (selectListEdit.suggestColumns) {
          parser.suggestColumns();
        }
        if (selectListEdit.suggestAggregateFunctions && (!hasDistinctOrAll || hasDistinctOrAll === 'ALL')) {
          parser.suggestAggregateFunctions();
          parser.suggestAnalyticFunctions();
        }
      };
  
      parser.suggestJoinConditions = function (details) {
        parser.yy.result.suggestJoinConditions = details || {};
        if (parser.yy.latestTablePrimaries && !parser.yy.result.suggestJoinConditions.tablePrimaries) {
          parser.yy.result.suggestJoinConditions.tablePrimaries = parser.yy.latestTablePrimaries.concat();
        }
      };
  
      parser.suggestJoins = function (details) {
        parser.yy.result.suggestJoins = details || {};
      };
  
      parser.valueExpressionSuggest = function (oppositeValueExpression, operator) {
        if (oppositeValueExpression && oppositeValueExpression.columnReference) {
          parser.suggestValues();
          parser.yy.result.colRef = {identifierChain: oppositeValueExpression.columnReference};
        }
        parser.suggestColumns();
        parser.suggestFunctions();
        var keywords = ['CASE'];
        if (parser.isHive() || typeof oppositeValueExpression === 'undefined' || typeof operator === 'undefined') {
          keywords = keywords.concat(['EXISTS', 'NOT']);
        }
        if (oppositeValueExpression && oppositeValueExpression.types[0] === 'NUMBER') {
          parser.applyTypeToSuggestions(['NUMBER']);
        } else if (parser.isImpala() && (typeof operator === 'undefined' || operator === '-' || operator === '+')) {
          keywords.push('INTERVAL');
        }
        parser.suggestKeywords(keywords);
      };
  
      parser.applyTypeToSuggestions = function (types) {
        if (types[0] === 'BOOLEAN') {
          return;
        }
        if (parser.yy.result.suggestFunctions && !parser.yy.result.suggestFunctions.types) {
          parser.yy.result.suggestFunctions.types = types;
        }
        if (parser.yy.result.suggestColumns && !parser.yy.result.suggestColumns.types) {
          parser.yy.result.suggestColumns.types = types;
        }
      };
  
      parser.findCaseType = function (whenThenList) {
        var types = {};
        whenThenList.caseTypes.forEach(function (valueExpression) {
          valueExpression.types.forEach(function (type) {
            types[type] = true;
          });
        });
        if (Object.keys(types).length === 1) {
          return {types: [Object.keys(types)[0]]};
        }
        return {types: ['T']};
      };
  
      parser.findReturnTypes = function (functionName) {
        return SqlFunctions.getReturnTypes(parser.yy.activeDialect, functionName.toLowerCase());
      };
  
      parser.applyArgumentTypesToSuggestions = function (functionName, position) {
        var foundArguments = SqlFunctions.getArgumentTypes(parser.yy.activeDialect, functionName.toLowerCase(), position);
        if (foundArguments.length == 0 && parser.yy.result.suggestColumns) {
          delete parser.yy.result.suggestColumns;
          delete parser.yy.result.suggestKeyValues;
          delete parser.yy.result.suggestValues;
          delete parser.yy.result.suggestFunctions;
          delete parser.yy.result.suggestIdentifiers;
          delete parser.yy.result.suggestKeywords;
        } else {
          parser.applyTypeToSuggestions(foundArguments);
        }
      };
  
      parser.commitLocations = function () {
        var i = parser.yy.locations.length;
        while (i--) {
          var location = parser.yy.locations[i];
  
          // Impala can have references to previous tables after FROM, i.e. FROM testTable t, t.testArray
          // In this testArray would be marked a type table so we need to switch it to column.
          if (location.type === 'table' && typeof location.identifierChain !== 'undefined' && location.identifierChain.length > 1 && parser.yy.latestTablePrimaries) {
            var found = parser.yy.latestTablePrimaries.filter(function (primary) {
              return equalIgnoreCase(primary.alias, location.identifierChain[0].name);
            });
            if (found.length > 0) {
              location.type = 'column';
            }
          }
  
          if (location.type === 'database' && typeof location.identifierChain !== 'undefined' && location.identifierChain.length > 0 && parser.yy.latestTablePrimaries) {
            var foundAlias = parser.yy.latestTablePrimaries.filter(function (primary) {
              return equalIgnoreCase(primary.alias, location.identifierChain[0].name);
            });
            if (foundAlias.length > 0) {
              // Impala complex reference in FROM clause, i.e. FROM testTable t, t.testMap tm
              location.type = 'table';
              parser.expandIdentifierChain(location, true);
            }
          }
  
          if (location.type === 'unknown') {
            if (typeof location.identifierChain !== 'undefined' && location.identifierChain.length > 0 && location.identifierChain.length <= 2 && parser.yy.latestTablePrimaries) {
              var found = parser.yy.latestTablePrimaries.filter(function (primary) {
                return equalIgnoreCase(primary.alias, location.identifierChain[0].name) || (primary.identifierChain && equalIgnoreCase(primary.identifierChain[0].name, location.identifierChain[0].name));
              });
              if (found.length > 0) {
                if (found[0].identifierChain.length > 1 && location.identifierChain.length === 1 && equalIgnoreCase(found[0].identifierChain[0].name, location.identifierChain[0].name)) {
                  location.type = 'database';
                } else if (found[0].alias && equalIgnoreCase(location.identifierChain[0].name, found[0].alias) && location.identifierChain.length > 1) {
                  location.type = 'column';
                  parser.expandIdentifierChain(location, true);
                } else if (!found[0].alias && found[0].identifierChain && equalIgnoreCase(location.identifierChain[0].name, found[0].identifierChain[found[0].identifierChain.length - 1].name) && location.identifierChain.length > 1) {
                  location.type = 'column';
                  parser.expandIdentifierChain(location, true);
                } else {
                  location.type = 'table';
                  parser.expandIdentifierChain(location, true);
                }
              } else {
                if (parser.yy.subQueries) {
                  found = parser.yy.subQueries.filter(function (subQuery) {
                    return equalIgnoreCase(subQuery.alias, location.identifierChain[0].name);
                  });
                  if (found.length > 0) {
                    location.type = 'subQuery';
                    location.identifierChain = [{subQuery: found[0].alias}];
                  }
                }
              }
            }
          }
  
          if (location.type === 'asterisk' && !location.linked) {
            if (parser.yy.latestTablePrimaries && parser.yy.latestTablePrimaries.length > 0) {
              location.tables = [];
              location.linked = false;
              parser.expandIdentifierChain(location, true);
              if (location.tables.length === 0) {
                parser.yy.locations.splice(i, 1);
              }
            } else {
              parser.yy.locations.splice(i, 1);
            }
          }
  
          if (location.type === 'table' && (typeof location.identifierChain === 'undefined' || location.identifierChain.length === 0)) {
            parser.yy.locations.splice(i, 1);
          }
  
          if (location.type === 'unknown') {
            location.type = 'column';
          }
          if (location.type === 'column') {
            if (parser.isHive() && !location.linked) {
              location.identifierChain = parser.expandLateralViews(parser.yy.lateralViews, location.identifierChain);
            }
            parser.expandIdentifierChain(location, true, true, true);
  
            if (typeof location.identifierChain === 'undefined') {
              parser.yy.locations.splice(i, 1);
            }
          }
          if (location.type === 'column' && location.identifierChain) {
            if (location.identifierChain.length > 1 && location.tables && location.tables.length > 0) {
              location.type = 'complex';
            }
          }
        }
        if (parser.yy.locations.length > 0) {
          parser.yy.allLocations = parser.yy.allLocations.concat(parser.yy.locations);
          parser.yy.locations = [];
        }
      };
  
      var prioritizeSuggestions = function () {
        parser.yy.result.lowerCase = parser.yy.lowerCase || false;
  
        var cteIndex = {};
  
        if (typeof parser.yy.latestCommonTableExpressions !== 'undefined') {
          parser.yy.latestCommonTableExpressions.forEach(function (cte) {
            cteIndex[cte.alias.toLowerCase()] = cte;
          })
        }
  
        SIMPLE_TABLE_REF_SUGGESTIONS.forEach(function (suggestionType) {
          if (suggestionType !== 'suggestAggregateFunctions' && typeof parser.yy.result[suggestionType] !== 'undefined' && parser.yy.result[suggestionType].tables.length === 0) {
            delete parser.yy.result[suggestionType];
          } else if (typeof parser.yy.result[suggestionType] !== 'undefined' && typeof parser.yy.result[suggestionType].tables !== 'undefined') {
            for (var i = parser.yy.result[suggestionType].tables.length - 1; i >= 0; i--) {
              var table = parser.yy.result[suggestionType].tables[i];
              if (table.identifierChain.length === 1 && typeof table.identifierChain[0].name !== 'undefined' && typeof cteIndex[table.identifierChain[0].name.toLowerCase()] !== 'undefined') {
                parser.yy.result[suggestionType].tables.splice(i, 1);
              }
            }
          }
        });
  
        if (typeof parser.yy.result.colRef !== 'undefined') {
          if (!parser.yy.result.colRef.linked || typeof parser.yy.result.colRef.identifierChain === 'undefined' || parser.yy.result.colRef.identifierChain.length === 0) {
            delete parser.yy.result.colRef;
            if (typeof parser.yy.result.suggestColRefKeywords !== 'undefined') {
              Object.keys(parser.yy.result.suggestColRefKeywords).forEach(function (type) {
                parser.yy.result.suggestKeywords = parser.yy.result.suggestKeywords.concat(parser.createWeightedKeywords(parser.yy.result.suggestColRefKeywords[type], -1));
              });
              delete parser.yy.result.suggestColRefKeywords;
            }
            if (parser.yy.result.suggestColumns && parser.yy.result.suggestColumns.types.length === 1 && parser.yy.result.suggestColumns.types[0] === 'COLREF') {
              parser.yy.result.suggestColumns.types = ['T'];
            }
            delete parser.yy.result.suggestValues;
          }
        }
  
        if (typeof parser.yy.result.colRef !== 'undefined') {
          if (!parser.yy.result.suggestValues && !parser.yy.result.suggestColRefKeywords &&
            (!parser.yy.result.suggestColumns ||
            parser.yy.result.suggestColumns.types[0] !== 'COLREF')) {
            delete parser.yy.result.colRef;
          }
        }
        if (typeof parser.yy.result.suggestIdentifiers !== 'undefined' && parser.yy.result.suggestIdentifiers.length > 0) {
          delete parser.yy.result.suggestTables;
          delete parser.yy.result.suggestDatabases;
        }
        if (typeof parser.yy.result.suggestColumns !== 'undefined') {
          var suggestColumns = parser.yy.result.suggestColumns;
          if (typeof suggestColumns.tables === 'undefined' || suggestColumns.tables.length === 0) {
            // Impala supports statements like SELECT * FROM tbl1, tbl2 WHERE db.tbl1.col = tbl2.bla
            if (parser.yy.result.suggestColumns.linked && parser.isImpala() && typeof suggestColumns.identifierChain !== 'undefined' && suggestColumns.identifierChain.length > 0) {
              if (suggestColumns.identifierChain.length === 1) {
                parser.yy.result.suggestTables = suggestColumns;
                delete parser.yy.result.suggestColumns
              } else {
                suggestColumns.tables = [{identifierChain: suggestColumns.identifierChain}];
                delete suggestColumns.identifierChain;
              }
            } else {
              delete parser.yy.result.suggestColumns;
              delete parser.yy.result.subQueries;
            }
          } else {
            delete parser.yy.result.suggestTables;
            delete parser.yy.result.suggestDatabases;
  
            suggestColumns.tables.forEach(function (table) {
              if (typeof table.identifierChain !== 'undefined' && table.identifierChain.length === 1 && typeof table.identifierChain[0].name !== 'undefined') {
                var cte = cteIndex[table.identifierChain[0].name.toLowerCase()];
                if (typeof cte !== 'undefined') {
                  delete table.identifierChain[0].name;
                  table.identifierChain[0].cte = cte.alias;
                }
              } else if (typeof table.identifierChain === 'undefined' && table.subQuery) {
                table.identifierChain = [{ subQuery: table.subQuery }];
                delete table.subQuery;
              }
            });
  
            if (typeof suggestColumns.identifierChain !== 'undefined' && suggestColumns.identifierChain.length === 0) {
              delete suggestColumns.identifierChain;
            }
          }
        } else {
          delete parser.yy.result.subQueries;
        }
  
        if (typeof parser.yy.result.suggestJoinConditions !== 'undefined') {
          if (typeof parser.yy.result.suggestJoinConditions.tables === 'undefined' || parser.yy.result.suggestJoinConditions.tables.length === 0) {
            delete parser.yy.result.suggestJoinConditions;
          }
        }
  
        if (typeof parser.yy.result.suggestTables !== 'undefined' && typeof parser.yy.latestCommonTableExpressions !== 'undefined') {
          var ctes = [];
          parser.yy.latestCommonTableExpressions.forEach(function (cte) {
            var suggestion = {name: cte.alias};
            if (parser.yy.result.suggestTables.prependFrom) {
              suggestion.prependFrom = true
            }
            if (parser.yy.result.suggestTables.prependQuestionMark) {
              suggestion.prependQuestionMark = true;
            }
            ctes.push(suggestion);
          });
          if (ctes.length > 0) {
            parser.yy.result.suggestCommonTableExpressions = ctes;
          }
        }
      };
  
      /**
       * Impala supports referencing maps and arrays in the the table reference list i.e.
       *
       *  SELECT m['foo'].bar.| FROM someDb.someTable t, t.someMap m;
       *
       * From this the tablePrimaries would look like:
       *
       * [ { alias: 't', identifierChain: [ { name: 'someDb' }, { name: 'someTable' } ] },
       *   { alias: 'm', identifierChain: [ { name: 't' }, { name: 'someMap' } ] } ]
       *
       * with an identifierChain from the select list:
       *
       * [ { name: 'm', keySet: true }, { name: 'bar' } ]
       *
       * Calling this would return an expanded identifierChain, given the above it would be:
       *
       * [ { name: 't' }, { name: 'someMap', keySet: true }, { name: 'bar' } ]
       */
      parser.expandImpalaIdentifierChain = function (tablePrimaries, identifierChain) {
        var expandedChain = identifierChain.concat(); // Clone in case it's called multiple times.
        if (typeof expandedChain === 'undefined' || expandedChain.length === 0) {
          return identifierChain;
        }
        var expand = function (identifier, expandedChain) {
          var foundPrimary = tablePrimaries.filter(function (tablePrimary) {
            return equalIgnoreCase(tablePrimary.alias, identifier);
          });
  
          if (foundPrimary.length === 1 && foundPrimary[0].identifierChain) {
            var parentPrimary = tablePrimaries.filter(function (tablePrimary) {
              return equalIgnoreCase(tablePrimary.alias, foundPrimary[0].identifierChain[0].name);
            });
            if (parentPrimary.length === 1) {
              var keySet = expandedChain[0].keySet;
              var secondPart = expandedChain.slice(1);
              var firstPart = [];
              // Clone to make sure we don't add keySet to the primaries
              foundPrimary[0].identifierChain.forEach(function (identifier) {
                firstPart.push({name: identifier.name});
              });
              if (keySet && firstPart.length > 0) {
                firstPart[firstPart.length - 1].keySet = true;
              }
  
              if (firstPart.length === 0 || typeof secondPart === 'undefined' || secondPart.length === 0) {
                return firstPart;
              }
              var result = firstPart.concat(secondPart);
              if (result.length > 0) {
                return expand(firstPart[0].name, result);
              } else {
                return result;
              }
            }
          }
          return expandedChain;
        };
        return expand(expandedChain[0].name, expandedChain);
      };
  
      parser.identifyPartials = function (beforeCursor, afterCursor) {
        var beforeMatch = beforeCursor.match(/[0-9a-zA-Z_]*$/);
        var afterMatch = afterCursor.match(/^[0-9a-zA-Z_]*(?:\((?:[^)]*\))?)?/);
        return {left: beforeMatch ? beforeMatch[0].length : 0, right: afterMatch ? afterMatch[0].length : 0};
      };
  
      parser.expandLateralViews = function (lateralViews, originalIdentifierChain, columnSuggestion) {
        var identifierChain = originalIdentifierChain.concat(); // Clone in case it's re-used
        var firstIdentifier = identifierChain[0];
        if (typeof lateralViews !== 'undefined') {
          lateralViews.concat().reverse().forEach(function (lateralView) {
            if (!lateralView.udtf.expression.columnReference) {
              return;
            }
            if (equalIgnoreCase(firstIdentifier.name, lateralView.tableAlias) && identifierChain.length > 1) {
              identifierChain.shift();
              firstIdentifier = identifierChain[0];
              if (columnSuggestion) {
                delete parser.yy.result.suggestKeywords;
              }
            } else if (equalIgnoreCase(firstIdentifier.name, lateralView.tableAlias) && identifierChain.length === 1 && typeof parser.yy.result.suggestColumns !== 'undefined') {
              if (columnSuggestion) {
                if (typeof parser.yy.result.suggestIdentifiers === 'undefined') {
                  parser.yy.result.suggestIdentifiers = [];
                }
                lateralView.columnAliases.forEach(function (columnAlias) {
                  parser.yy.result.suggestIdentifiers.push({name: columnAlias, type: 'alias'});
                });
                delete parser.yy.result.suggestColumns;
                delete parser.yy.result.suggestKeywords;
              }
              return identifierChain;
            }
            if (lateralView.columnAliases.indexOf(firstIdentifier.name) !== -1) {
              if (lateralView.columnAliases.length === 2 && lateralView.udtf.function.toLowerCase() === 'explode' && equalIgnoreCase(firstIdentifier.name, lateralView.columnAliases[0])) {
                identifierChain[0] = {name: 'key'};
              } else if (lateralView.columnAliases.length === 2 && lateralView.udtf.function.toLowerCase() === 'explode' && equalIgnoreCase(firstIdentifier.name, lateralView.columnAliases[1])) {
                identifierChain[0] = {name: 'value'};
              } else {
                identifierChain[0] = {name: 'item'};
              }
              identifierChain = lateralView.udtf.expression.columnReference.concat(identifierChain);
              firstIdentifier = identifierChain[0];
            }
          });
        }
        return identifierChain;
      };
  
      var addCleanTablePrimary = function (tables, tablePrimary) {
        if (tablePrimary.alias) {
          tables.push({alias: tablePrimary.alias, identifierChain: tablePrimary.identifierChain});
        } else {
          tables.push({identifierChain: tablePrimary.identifierChain});
        }
      };
  
      parser.expandIdentifierChain = function (wrapper, anyOwner, isColumnWrapper, isColumnLocation) {
        if (typeof wrapper.identifierChain === 'undefined' || typeof parser.yy.latestTablePrimaries === 'undefined') {
          return;
        }
        var identifierChain = wrapper.identifierChain.concat();
        var tablePrimaries = parser.yy.latestTablePrimaries;
  
        if (tablePrimaries.length === 0) {
          delete wrapper.identifierChain;
          return;
        }
  
        if (identifierChain.length > 0 && identifierChain[identifierChain.length - 1].asterisk) {
          var tables = [];
          tablePrimaries.forEach(function (tablePrimary) {
            if (identifierChain.length > 1 && !tablePrimary.subQueryAlias) {
              if (identifierChain.length === 2 && equalIgnoreCase(tablePrimary.alias, identifierChain[0].name)) {
                addCleanTablePrimary(tables, tablePrimary);
              } else if (identifierChain.length === 2 && equalIgnoreCase(tablePrimary.identifierChain[0].name, identifierChain[0].name)) {
                addCleanTablePrimary(tables, tablePrimary);
              } else if (identifierChain.length === 3 && tablePrimary.identifierChain.length > 1 &&
                equalIgnoreCase(tablePrimary.identifierChain[0].name, identifierChain[0].name) &&
                equalIgnoreCase(tablePrimary.identifierChain[1].name, identifierChain[1].name)) {
                addCleanTablePrimary(tables, tablePrimary);
              }
            } else {
              if (tablePrimary.subQueryAlias) {
                tables.push({identifierChain: [{subQuery: tablePrimary.subQueryAlias}]});
              } else {
                addCleanTablePrimary(tables, tablePrimary);
              }
            }
          });
          // Possible Joins
          if (tables.length > 0) {
            wrapper.tables = tables;
            delete wrapper.identifierChain;
            return;
          }
        }
  
        if (!anyOwner) {
          tablePrimaries = filterTablePrimariesForOwner(wrapper.owner);
        }
        // Impala can have references to maps or array, i.e. FROM table t, t.map m
        // We need to replace those in the identifierChain
        if (parser.isImpala()) {
          var lengthBefore = identifierChain.length;
          identifierChain = parser.expandImpalaIdentifierChain(tablePrimaries, identifierChain);
          // Change type of any locations marked as table
          if (wrapper.type === 'table' && identifierChain.length > lengthBefore) {
            wrapper.type = 'column';
          }
          wrapper.identifierChain = identifierChain;
        }
        // Expand exploded views in the identifier chain
        if (parser.isHive() && identifierChain.length > 0) {
          identifierChain = parser.expandLateralViews(parser.yy.lateralViews, identifierChain);
          wrapper.identifierChain = identifierChain;
        }
  
        // IdentifierChain contains a possibly started identifier or empty, example: a.b.c = ['a', 'b', 'c']
        // Reduce the tablePrimaries to the one that matches the first identifier if found
        var foundPrimary;
        var doubleMatch = false;
        var aliasMatch = false;
        if (identifierChain.length > 0) {
          for (var i = 0; i < tablePrimaries.length; i++) {
            if (tablePrimaries[i].subQueryAlias) {
              if (equalIgnoreCase(tablePrimaries[i].subQueryAlias, identifierChain[0].name)) {
                foundPrimary = tablePrimaries[i];
              }
            } else if (equalIgnoreCase(tablePrimaries[i].alias, identifierChain[0].name)) {
              foundPrimary = tablePrimaries[i];
              aliasMatch = true;
              break;
            } else if (tablePrimaries[i].identifierChain.length > 1 && identifierChain.length > 1 &&
              equalIgnoreCase(tablePrimaries[i].identifierChain[0].name, identifierChain[0].name) &&
              equalIgnoreCase(tablePrimaries[i].identifierChain[1].name, identifierChain[1].name)) {
              foundPrimary = tablePrimaries[i];
              doubleMatch = true;
              break;
            } else if (!foundPrimary && equalIgnoreCase(tablePrimaries[i].identifierChain[0].name, identifierChain[0].name) && identifierChain.length > (isColumnLocation ? 1 : 0)) {
              foundPrimary = tablePrimaries[i];
              // No break as first two can still match.
            } else if (!foundPrimary && tablePrimaries[i].identifierChain.length > 1
              && equalIgnoreCase(tablePrimaries[i].identifierChain[tablePrimaries[i].identifierChain.length - 1].name, identifierChain[0].name)) {
              // This is for the case SELECT baa. FROM bla.baa, blo.boo;
              foundPrimary = tablePrimaries[i];
              break;
            }
          }
        }
  
        if (foundPrimary) {
          identifierChain.shift();
          if (doubleMatch) {
            identifierChain.shift();
          }
        } else if (tablePrimaries.length === 1 && !isColumnWrapper) {
          foundPrimary = tablePrimaries[0];
        }
  
        if (foundPrimary) {
          if (isColumnWrapper) {
            wrapper.identifierChain = identifierChain;
            if (foundPrimary.subQueryAlias) {
              wrapper.tables = [{ subQuery: foundPrimary.subQueryAlias }];
            } else if (foundPrimary.alias) {
              if (!isColumnLocation && isColumnWrapper && aliasMatch) {
                // TODO: add alias on table in suggestColumns (needs support in sqlAutocomplete3.js)
                // the case is: SELECT cu.| FROM customers cu;
                // This prevents alias from being added automatically in sqlAutocompleter3.js
                wrapper.tables = [{ identifierChain: foundPrimary.identifierChain }];
              } else {
                wrapper.tables = [{ identifierChain: foundPrimary.identifierChain, alias: foundPrimary.alias }];
              }
            } else {
              wrapper.tables = [{ identifierChain: foundPrimary.identifierChain }];
            }
          } else {
            if (foundPrimary.subQueryAlias) {
              identifierChain.unshift({ subQuery: foundPrimary.subQueryAlias });
            } else {
              identifierChain = foundPrimary.identifierChain.concat(identifierChain);
            }
            if (wrapper.tables) {
              wrapper.tables.push({identifierChain: identifierChain});
              delete wrapper.identifierChain;
            } else {
              wrapper.identifierChain = identifierChain;
            }
          }
        } else {
          if (isColumnWrapper) {
            wrapper.tables = [];
          }
          tablePrimaries.forEach(function (tablePrimary) {
            var targetTable = tablePrimary.subQueryAlias ? { subQuery: tablePrimary.subQueryAlias } : { identifierChain: tablePrimary.identifierChain } ;
            if (tablePrimary.alias) {
              targetTable.alias = tablePrimary.alias;
            }
            if (wrapper.tables) {
              wrapper.tables.push(targetTable)
            }
          });
        }
        delete wrapper.owner;
        wrapper.linked = true;
      };
  
      var suggestLateralViewAliasesAsIdentifiers = function () {
        if (typeof parser.yy.lateralViews === 'undefined' || parser.yy.lateralViews.length === 0) {
          return;
        }
        if (typeof parser.yy.result.suggestIdentifiers === 'undefined') {
          parser.yy.result.suggestIdentifiers = [];
        }
        parser.yy.lateralViews.forEach(function (lateralView) {
          if (typeof lateralView.tableAlias !== 'undefined') {
            parser.yy.result.suggestIdentifiers.push({name: lateralView.tableAlias + '.', type: 'alias'});
          }
          lateralView.columnAliases.forEach(function (columnAlias) {
            parser.yy.result.suggestIdentifiers.push({name: columnAlias, type: 'alias'});
          });
        });
        if (parser.yy.result.suggestIdentifiers.length === 0) {
          delete parser.yy.result.suggestIdentifiers;
        }
      };
  
      var filterTablePrimariesForOwner = function (owner) {
        var result = [];
        parser.yy.latestTablePrimaries.forEach(function (primary) {
          if (typeof owner === 'undefined' && typeof primary.owner === 'undefined') {
            result.push(primary);
          } else if (owner === primary.owner) {
            result.push(primary);
          }
        });
        return result;
      };
  
      var convertTablePrimariesToSuggestions = function (tablePrimaries) {
        var tables = [];
        var identifiers = [];
        tablePrimaries.forEach(function (tablePrimary) {
          if (tablePrimary.identifierChain && tablePrimary.identifierChain.length > 0) {
            var table = {identifierChain: tablePrimary.identifierChain};
            if (tablePrimary.alias) {
              table.alias = tablePrimary.alias;
              identifiers.push({name: table.alias + '.', type: 'alias'});
              if (parser.isImpala()) {
                var testForImpalaAlias = [{name: table.alias}];
                var result = parser.expandImpalaIdentifierChain(tablePrimaries, testForImpalaAlias);
                if (result.length > 1) {
                  // Continue if it's a reference to a complex type
                  return;
                }
              }
            } else {
              var lastIdentifier = tablePrimary.identifierChain[tablePrimary.identifierChain.length - 1];
              if (typeof lastIdentifier.name !== 'undefined') {
                identifiers.push({name: lastIdentifier.name + '.', type: 'table'});
              } else if (typeof lastIdentifier.subQuery !== 'undefined') {
                identifiers.push({name: lastIdentifier.subQuery + '.', type: 'sub-query'});
              }
            }
            tables.push(table);
          } else if (tablePrimary.subQueryAlias) {
            identifiers.push({name: tablePrimary.subQueryAlias + '.', type: 'sub-query'});
            tables.push({identifierChain: [{subQuery: tablePrimary.subQueryAlias}]});
          }
        });
        if (identifiers.length > 0) {
          if (typeof parser.yy.result.suggestIdentifiers === 'undefined') {
            parser.yy.result.suggestIdentifiers = identifiers;
          } else {
            parser.yy.result.suggestIdentifiers = identifiers.concat(parser.yy.result.suggestIdentifiers);
          }
        }
        parser.yy.result.suggestColumns.tables = tables;
        if (parser.yy.result.suggestColumns.identifierChain && parser.yy.result.suggestColumns.identifierChain.length == 0) {
          delete parser.yy.result.suggestColumns.identifierChain;
        }
        parser.yy.result.suggestColumns.linked = true;
      };
  
      var linkTablePrimaries = function () {
        if (!parser.yy.cursorFound || typeof parser.yy.latestTablePrimaries === 'undefined') {
          return;
        }
  
        SIMPLE_TABLE_REF_SUGGESTIONS.forEach(function (suggestionType) {
          if (typeof parser.yy.result[suggestionType] !== 'undefined' && parser.yy.result[suggestionType].tablePrimaries && !parser.yy.result[suggestionType].linked) {
            parser.yy.result[suggestionType].tables = [];
            parser.yy.result[suggestionType].tablePrimaries.forEach(function (tablePrimary) {
              if (!tablePrimary.subQueryAlias) {
                parser.yy.result[suggestionType].tables.push(tablePrimary.alias ? {
                  identifierChain: tablePrimary.identifierChain.concat(),
                  alias: tablePrimary.alias
                } : {identifierChain: tablePrimary.identifierChain.concat()});
              }
            });
            delete parser.yy.result[suggestionType].tablePrimaries;
            parser.yy.result[suggestionType].linked = true;
          }
        });
  
        if (typeof parser.yy.result.suggestColumns !== 'undefined' && !parser.yy.result.suggestColumns.linked) {
          var tablePrimaries = filterTablePrimariesForOwner(parser.yy.result.suggestColumns.owner);
          if (!parser.yy.result.suggestColumns.tables) {
            parser.yy.result.suggestColumns.tables = [];
          }
          if (parser.yy.subQueries.length > 0) {
            parser.yy.result.subQueries = parser.yy.subQueries;
          }
          if (typeof parser.yy.result.suggestColumns.identifierChain === 'undefined' || parser.yy.result.suggestColumns.identifierChain.length === 0) {
            if (tablePrimaries.length > 1) {
              convertTablePrimariesToSuggestions(tablePrimaries);
            } else {
              suggestLateralViewAliasesAsIdentifiers();
              if (tablePrimaries.length == 1 && (tablePrimaries[0].alias || tablePrimaries[0].subQueryAlias)) {
                convertTablePrimariesToSuggestions(tablePrimaries);
              }
              parser.expandIdentifierChain(parser.yy.result.suggestColumns, false, true);
            }
          } else {
            // Expand exploded views in the identifier chain
            if (parser.isHive() && !parser.yy.result.suggestColumns.linked) {
              var originalLength = parser.yy.result.suggestColumns.identifierChain.length;
              parser.yy.result.suggestColumns.identifierChain = parser.expandLateralViews(parser.yy.lateralViews, parser.yy.result.suggestColumns.identifierChain, true);
              // Drop '*' keyword for lateral views
              if (typeof parser.yy.result.suggestColumns !== 'undefined') {
                if (parser.yy.result.suggestColumns.identifierChain.length > originalLength &&
                  typeof parser.yy.result.suggestKeywords !== 'undefined' &&
                  parser.yy.result.suggestKeywords.length === 1 &&
                  parser.yy.result.suggestKeywords[0].value === '*') {
                  delete parser.yy.result.suggestKeywords;
                }
                parser.expandIdentifierChain(parser.yy.result.suggestColumns, false, true);
              }
            } else {
              parser.expandIdentifierChain(parser.yy.result.suggestColumns, false, true);
            }
          }
        }
  
        if (typeof parser.yy.result.colRef !== 'undefined' && !parser.yy.result.colRef.linked) {
          parser.expandIdentifierChain(parser.yy.result.colRef);
  
          var primaries = filterTablePrimariesForOwner();
          if (primaries.length === 0 || (primaries.length > 1 && parser.yy.result.colRef.identifierChain.length === 1)) {
            parser.yy.result.colRef.identifierChain = [];
          }
        }
        if (typeof parser.yy.result.suggestKeyValues !== 'undefined' && !parser.yy.result.suggestKeyValues.linked) {
          parser.expandIdentifierChain(parser.yy.result.suggestKeyValues);
        }
      };
  
      parser.getSubQuery = function (cols) {
        var columns = [];
        cols.selectList.forEach(function (col) {
          var result = {};
          if (col.alias) {
            result.alias = col.alias;
          }
          if (col.valueExpression && col.valueExpression.columnReference) {
            result.identifierChain = col.valueExpression.columnReference
          } else if (col.asterisk) {
            result.identifierChain = [{asterisk: true}];
          }
          if (col.valueExpression && col.valueExpression.types && col.valueExpression.types.length === 1) {
            result.type = col.valueExpression.types[0];
          }
  
          columns.push(result);
        });
  
        return {
          columns: columns
        };
      };
  
      parser.addTablePrimary = function (ref) {
        if (typeof parser.yy.latestTablePrimaries === 'undefined') {
          parser.yy.latestTablePrimaries = [];
        }
        parser.yy.latestTablePrimaries.push(ref);
      };
  
      parser.suggestFileFormats = function () {
        if (parser.isHive()) {
          parser.suggestKeywords(['AVRO', 'INPUTFORMAT', 'ORC', 'PARQUET', 'RCFILE', 'SEQUENCEFILE', 'TEXTFILE']);
        } else {
          parser.suggestKeywords(['AVRO', 'KUDU', 'PARQUET', 'RCFILE', 'SEQUENCEFILE', 'TEXTFILE']);
        }
      };
  
      parser.getKeywordsForOptionalsLR = function (optionals, keywords, override) {
        var result = [];
  
        for (var i = 0; i < optionals.length; i++) {
          if (!optionals[i] && (typeof override === 'undefined' || override[i])) {
            if (keywords[i] instanceof Array) {
              result = result.concat(keywords[i]);
            } else {
              result.push(keywords[i]);
            }
          } else if (optionals[i]) {
            break;
          }
        }
        return result;
      };
  
      parser.suggestDdlAndDmlKeywords = function (extraKeywords) {
        var keywords = ['ALTER', 'CREATE', 'DESCRIBE', 'DROP', 'GRANT', 'INSERT', 'REVOKE', 'SELECT', 'SET', 'SHOW', 'TRUNCATE', 'UPDATE', 'USE', 'WITH'];
  
        if (extraKeywords) {
          keywords = keywords.concat(extraKeywords);
        }
  
        if (parser.isHive()) {
          keywords = keywords.concat(['ANALYZE TABLE', 'DELETE', 'EXPORT', 'IMPORT', 'LOAD', 'MSCK', 'RELOAD FUNCTION', 'RESET']);
        }
  
        if (parser.isImpala()) {
          keywords = keywords.concat(['COMPUTE', 'INVALIDATE METADATA', 'LOAD', 'REFRESH']);
        }
  
        parser.suggestKeywords(keywords);
      };
  
      parser.checkForSelectListKeywords = function (selectList) {
        if (selectList.length === 0) {
          return;
        }
        var last = selectList[selectList.length - 1];
        if (!last || !last.valueExpression) {
          return;
        }
        var valueExpressionKeywords = parser.getValueExpressionKeywords(last.valueExpression);
        var keywords = [];
        if (last.suggestKeywords) {
          keywords = keywords.concat(last.suggestKeywords);
        }
        if (valueExpressionKeywords.suggestKeywords) {
          keywords = keywords.concat(valueExpressionKeywords.suggestKeywords);
        }
        if (valueExpressionKeywords.suggestColRefKeywords) {
          parser.suggestColRefKeywords(valueExpressionKeywords.suggestColRefKeywords);
          parser.addColRefIfExists(last.valueExpression);
        }
        if (!last.alias) {
          keywords.push('AS');
        }
        if (keywords.length > 0) {
          parser.suggestKeywords(keywords);
        }
      };
  
      parser.checkForKeywords = function (expression) {
        if (expression) {
          if (expression.suggestKeywords && expression.suggestKeywords.length > 0) {
            parser.suggestKeywords(expression.suggestKeywords);
          }
          if (expression.suggestColRefKeywords) {
            parser.suggestColRefKeywords(expression.suggestColRefKeywords);
            parser.addColRefIfExists(expression);
          }
        }
      };
  
      parser.createWeightedKeywords = function (keywords, weight) {
        var result = [];
        keywords.forEach(function (keyword) {
          if (typeof keyword.weight !== 'undefined') {
            keyword.weight = weight + (keyword.weight / 10);
            result.push(keyword);
          } else {
            result.push({value: keyword, weight: weight});
          }
        });
        return result;
      };
  
      parser.suggestKeywords = function (keywords) {
        var weightedKeywords = [];
        if (keywords.length == 0) {
          return;
        }
        keywords.forEach(function (keyword) {
          if (typeof keyword.weight !== 'undefined') {
            weightedKeywords.push(keyword);
          } else {
            weightedKeywords.push({value: keyword, weight: -1})
          }
        });
        weightedKeywords.sort(function (a, b) {
          if (a.weight !== b.weight) {
            return b.weight - a.weight;
          }
          return a.value.localeCompare(b.value);
        });
        parser.yy.result.suggestKeywords = weightedKeywords;
      };
  
      parser.suggestColRefKeywords = function (colRefKeywords) {
        parser.yy.result.suggestColRefKeywords = colRefKeywords;
      };
  
      parser.suggestTablesOrColumns = function (identifier) {
        if (typeof parser.yy.latestTablePrimaries == 'undefined') {
          parser.suggestTables({identifierChain: [{name: identifier}]});
          return;
        }
        var tableRef = parser.yy.latestTablePrimaries.filter(function (tablePrimary) {
          return equalIgnoreCase(tablePrimary.alias, identifier);
        });
        if (tableRef.length > 0) {
          parser.suggestColumns({identifierChain: [{name: identifier}]});
        } else {
          parser.suggestTables({identifierChain: [{name: identifier}]});
        }
      };
  
      parser.suggestFunctions = function (details) {
        parser.yy.result.suggestFunctions = details || {};
      };
  
      parser.suggestAggregateFunctions = function () {
        var primaries = [];
        var aliases = {};
        parser.yy.latestTablePrimaries.forEach(function (primary) {
          if (typeof primary.alias !== 'undefined') {
            aliases[primary.alias] = true;
          }
          // Drop if the first one refers to a table alias (...FROM tbl t, t.map tm ...)
          if (typeof primary.identifierChain !== 'undefined' && !aliases[primary.identifierChain[0].name] && typeof primary.owner === 'undefined') {
            primaries.push(primary);
          }
        });
        parser.yy.result.suggestAggregateFunctions = {tablePrimaries: primaries};
      };
  
      parser.suggestAnalyticFunctions = function () {
        parser.yy.result.suggestAnalyticFunctions = true;
      };
  
      parser.suggestColumns = function (details) {
        if (typeof details === 'undefined') {
          details = {identifierChain: []};
        } else if (typeof details.identifierChain === 'undefined') {
          details.identifierChain = [];
        }
        parser.yy.result.suggestColumns = details;
      };
  
      parser.suggestGroupBys = function (details) {
        parser.yy.result.suggestGroupBys = details || {};
      };
  
      parser.suggestOrderBys = function (details) {
        parser.yy.result.suggestOrderBys = details || {};
      };
  
      parser.suggestFilters = function (details) {
        parser.yy.result.suggestFilters = details || {};
      };
  
      parser.suggestKeyValues = function (details) {
        parser.yy.result.suggestKeyValues = details || {};
      };
  
      parser.suggestTables = function (details) {
        parser.yy.result.suggestTables = details || {};
      };
  
      var adjustLocationForCursor = function (location) {
        // columns are 0-based and lines not, so add 1 to cols
        var 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) {
            var 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
        var 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;
        }
        var 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) {
          var 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.addHdfsLocation = function (location, path) {
        parser.yy.locations.push({
          type: 'hdfs',
          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.addAsteriskLocation = function (location, identifierChain) {
        parser.yy.locations.push({
          type: 'asterisk',
          location: adjustLocationForCursor(location),
          identifierChain: identifierChain
        });
      };
  
      parser.addColumnLocation = function (location, identifierChain) {
        parser.yy.locations.push({
          type: 'column',
          location: adjustLocationForCursor(location),
          identifierChain: identifierChain
        });
      };
  
      parser.addUnknownLocation = function (location, identifierChain) {
        parser.yy.locations.push({
          type: 'unknown',
          location: adjustLocationForCursor(location),
          identifierChain: identifierChain
        });
      };
  
      parser.suggestDatabases = function (details) {
        parser.yy.result.suggestDatabases = details || {};
      };
  
      parser.suggestHdfs = function (details) {
        parser.yy.result.suggestHdfs = details || {};
      };
  
      parser.suggestValues = function (details) {
        parser.yy.result.suggestValues = details || {};
      };
  
      parser.determineCase = function (text) {
        if (!parser.yy.caseDetermined) {
          parser.yy.lowerCase = text.toLowerCase() === text;
          parser.yy.caseDetermined = true;
        }
      };
  
      parser.handleQuotedValueWithCursor = function (lexer, yytext, yylloc, quoteChar) {
        if (yytext.indexOf('\u2020') !== -1 || yytext.indexOf('\u2021') !== -1) {
          parser.yy.partialCursor = yytext.indexOf('\u2021') !== -1;
          var 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
          };
          var remainder = yytext.substring(cursorIndex + 1);
          var 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;
      };
  
      var lexerModified = false;
  
      /**
       * Main parser function
       */
      parser.parseSql = function (beforeCursor, afterCursor, dialect, debug) {
        // Jison counts CRLF as two lines in the locations
        beforeCursor = beforeCursor.replace(/\r\n|\n\r/gm, '\n');
        afterCursor = afterCursor.replace(/\r\n|\n\r/gm, '\n');
        parser.yy.result = {locations: []};
        parser.yy.lowerCase = false;
        parser.yy.locations = [];
        parser.yy.allLocations = [];
        parser.yy.subQueries = [];
        parser.yy.errors = [];
        parser.yy.selectListAliases = [];
  
        parser.yy.locationsStack = [];
        parser.yy.primariesStack = [];
        parser.yy.lateralViewsStack = [];
        parser.yy.subQueriesStack = [];
        parser.yy.resultStack = [];
        parser.yy.selectListAliasesStack = [];
  
        delete parser.yy.caseDetermined;
        delete parser.yy.cursorFound;
        delete parser.yy.partialCursor;
  
        parser.prepareNewStatement();
  
        var REASONABLE_SURROUNDING_LENGTH = 150000; // About 3000 lines before and after
  
        if (beforeCursor.length > REASONABLE_SURROUNDING_LENGTH) {
          if ((beforeCursor.length - beforeCursor.lastIndexOf(';')) > REASONABLE_SURROUNDING_LENGTH) {
            // Bail out if the last complete statement is more than 150000 chars before
            return {};
          }
          // Cut it at the first statement found within 150000 chars before
          var lastReasonableChunk = beforeCursor.substring(beforeCursor.length - REASONABLE_SURROUNDING_LENGTH);
          beforeCursor = lastReasonableChunk.substring(lastReasonableChunk.indexOf(';') + 1);
        }
  
        if (afterCursor.length > REASONABLE_SURROUNDING_LENGTH) {
          if ((afterCursor.length - afterCursor.indexOf(';')) > REASONABLE_SURROUNDING_LENGTH) {
            // No need to bail out for what's comes after, we can still get keyword completion
            afterCursor = '';
          } else {
            // Cut it at the last statement found within 150000 chars after
            var firstReasonableChunk = afterCursor.substring(0, REASONABLE_SURROUNDING_LENGTH);
            afterCursor = firstReasonableChunk.substring(0, firstReasonableChunk.lastIndexOf(';'));
          }
        }
  
        parser.yy.partialLengths = parser.identifyPartials(beforeCursor, afterCursor);
  
        if (parser.yy.partialLengths.left > 0) {
          beforeCursor = beforeCursor.substring(0, beforeCursor.length - parser.yy.partialLengths.left);
        }
  
        if (parser.yy.partialLengths.right > 0) {
          afterCursor = afterCursor.substring(parser.yy.partialLengths.right);
        }
  
        parser.yy.activeDialect = (dialect !== 'hive' && dialect !== 'impala') ? undefined : dialect;
  
        // Hack to set the inital state of the lexer without first having to hit a token
        // has to be done as the first token found can be dependant on dialect
        if (!lexerModified) {
          var originalSetInput = parser.lexer.setInput;
          parser.lexer.setInput = function (input, yy) {
            var lexer = originalSetInput.bind(parser.lexer)(input, yy);
            if (typeof parser.yy.activeDialect !== 'undefined') {
              lexer.begin(parser.yy.activeDialect);
            }
            return lexer;
          };
          lexerModified = true;
        }
  
        var result;
        try {
          // Add |CURSOR| or |PARTIAL_CURSOR| to represent the different cursor states in the lexer
          result = parser.parse(beforeCursor + (beforeCursor.length == 0 || /[\s\(]$$/.test(beforeCursor) ? ' \u2020 ' : '\u2021') + afterCursor);
        } catch (err) {
          // On any error try to at least return any existing result
          if (typeof parser.yy.result === 'undefined') {
            throw err;
          }
          if (debug) {
            console.log(err);
            console.error(err.stack);
          }
          result = parser.yy.result;
        }
        if (parser.yy.errors.length > 0) {
          parser.yy.result.errors = parser.yy.errors;
          if (debug) {
            console.log(parser.yy.errors);
          }
        }
        try {
          linkTablePrimaries();
          parser.commitLocations();
          // Clean up and prioritize
          prioritizeSuggestions();
        } catch (err) {
          if (debug) {
            console.log(err);
            console.error(err.stack);
          }
        }
  
  
        parser.yy.allLocations.sort(function (a, b) {
          if (a.location.first_line !== b.location.first_line) {
            return a.location.first_line - b.location.first_line;
          }
          return a.location.first_column - b.location.first_column;
        });
        parser.yy.result.locations = parser.yy.allLocations;
  
        parser.yy.result.locations.forEach(function (location) {
          delete location.linked;
        });
        if (typeof parser.yy.result.suggestColumns !== 'undefined') {
          delete parser.yy.result.suggestColumns.linked;
        }
  
        SIMPLE_TABLE_REF_SUGGESTIONS.forEach(function (suggestionType) {
          if (typeof parser.yy.result[suggestionType] !== 'undefined') {
            delete parser.yy.result[suggestionType].linked;
          }
        });
  
        if (typeof parser.yy.result.colRef !== 'undefined') {
          delete parser.yy.result.colRef.linked;
        }
        if (typeof parser.yy.result.suggestKeyValues !== 'undefined') {
          delete parser.yy.result.suggestKeyValues.linked;
        }
  
        if (typeof result.error !== 'undefined' && typeof result.error.expected !== 'undefined') {
          // Remove any expected tokens from other dialects, jison doesn't remove tokens from other lexer states.
          var actualExpected = {};
          result.error.expected.forEach(function (expected) {
            var match = expected.match(/\<([a-z]+)\>(.*)/);
            if (match !== null) {
              if (typeof parser.yy.activeDialect !== 'undefined' && parser.yy.activeDialect === match[1]) {
                actualExpected[("'" + match[2])] = true;
              }
            } else if (expected.indexOf('CURSOR') == -1) {
              actualExpected[expected] = true;
            }
          });
          result.error.expected = Object.keys(actualExpected);
        }
  
        if (typeof result.error !== 'undefined' && result.error.recoverable) {
          delete result.error;
        }
  
        // Adjust all the statement locations to include white space surrounding them
        var lastStatementLocation = null;
        result.locations.forEach(function (location) {
          if (location.type === 'statement') {
            if (lastStatementLocation === null) {
              location.location.first_line = 1;
              location.location.first_column = 1;
            } else {
              location.location.first_line = lastStatementLocation.location.last_line;
              location.location.first_column = lastStatementLocation.location.last_column + 1;
            }
            lastStatementLocation = location;
          }
        });
  
        return result;
      };
    };
  
    return {
      initSqlParser: initSqlParser
    };
  })();
  