Sfoglia il codice sorgente

[editor] Base SparkSQL autocompleter and syntax checker

Johan Åhlén 3 anni fa
parent
commit
55aa213cb4

+ 2 - 0
.eslintignore

@@ -41,4 +41,6 @@
 /desktop/core/src/desktop/js/parse/sql/dasksql/dasksqlAutocompleteParser.js
 /desktop/core/src/desktop/js/parse/sql/dasksql/dasksqlSyntaxParser.js
 /desktop/core/src/desktop/js/parse/sql/dasksql/spec/dasksqlAutocompleteParser_Locations_Spec.js
+/desktop/core/src/desktop/js/parse/sql/sparksql/sparksqlAutocompleteParser.js
+/desktop/core/src/desktop/js/parse/sql/sparksql/sparksqlSyntaxParser.js
 **/node_modules/**

+ 2 - 3
desktop/core/src/desktop/js/parse/sql/generic/jison/sql_main.test.json

@@ -1,11 +1,10 @@
 [
   {
     "namePrefix": "should suggest keywords",
-    "beforeCursor": ";;",
+    "beforeCursor": "[;;",
     "afterCursor": "",
     "containsKeywords": [
-      "SELECT",
-      "WITH"
+      "SELECT"
     ],
     "expectedResult": {
       "lowerCase": false

+ 229 - 0
desktop/core/src/desktop/js/parse/sql/sparksql/jison/sql.jisonlex

@@ -0,0 +1,229 @@
+// 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.
+
+%options case-insensitive flex
+%s between
+%x hdfs doubleQuotedValue singleQuotedValue backtickedValue
+%%
+
+\s                                         { /* skip whitespace */ }
+'--'.*                                     { /* skip comments */ }
+[/][*][^*]*[*]+([^/*][^*]*[*]+)*[/]        { /* skip comments */ }
+
+'\u2020'                                   { parser.yy.partialCursor = false; parser.yy.cursorFound = yylloc; return 'CURSOR'; }
+'\u2021'                                   { parser.yy.partialCursor = true; parser.yy.cursorFound = yylloc; return 'PARTIAL_CURSOR'; }
+
+<between>'AND'                             { this.popState(); return 'BETWEEN_AND'; }
+
+// Reserved Keywords
+'ALL'                                      { return 'ALL'; }
+'ALTER'                                    { parser.determineCase(yytext); parser.addStatementTypeLocation('ALTER', yylloc, yy.lexer.upcomingInput()); return 'ALTER'; }
+'AND'                                      { return 'AND'; }
+'AS'                                       { return 'AS'; }
+'ASC'                                      { return 'ASC'; }
+'BETWEEN'                                  { this.begin('between'); return 'BETWEEN'; }
+'BIGINT'                                   { return 'BIGINT'; }
+'BOOLEAN'                                  { return 'BOOLEAN'; }
+'BY'                                       { return 'BY'; }
+'CASCADE'                                  { return 'CASCADE'; }
+'CASE'                                     { return 'CASE'; }
+'CHAR'                                     { return 'CHAR'; }
+'COLUMNS'                                  { return 'COLUMNS'; }
+'COMMENT'                                  { return 'COMMENT'; }
+'CREATE'                                   { parser.determineCase(yytext); return 'CREATE'; }
+'CROSS'                                    { return 'CROSS'; }
+'CURRENT'                                  { return 'CURRENT'; }
+'DATABASE'                                 { return 'DATABASE'; }
+'DECIMAL'                                  { return 'DECIMAL'; }
+'DESC'                                     { return 'DESC'; }
+'DISTINCT'                                 { return 'DISTINCT'; }
+'DIV'                                      { return 'ARITHMETIC_OPERATOR'; }
+'DOUBLE'                                   { return 'DOUBLE'; }
+'DROP'                                     { parser.determineCase(yytext); parser.addStatementTypeLocation('DROP', yylloc, yy.lexer.upcomingInput()); return 'DROP'; }
+'ELSE'                                     { return 'ELSE'; }
+'END'                                      { return 'END'; }
+'EXISTS'                                   { parser.yy.correlatedSubQuery = true; return 'EXISTS'; }
+'FALSE'                                    { return 'FALSE'; }
+'FLOAT'                                    { return 'FLOAT'; }
+'FOLLOWING'                                { return 'FOLLOWING'; }
+'FROM'                                     { parser.determineCase(yytext); return 'FROM'; }
+'FULL'                                     { return 'FULL'; }
+'GROUP'                                    { return 'GROUP'; }
+'HAVING'                                   { return 'HAVING'; }
+'IF'                                       { return 'IF'; }
+'IN'                                       { return 'IN'; }
+'INNER'                                    { return 'INNER'; }
+'INSERT'                                   { return 'INSERT'; }
+'INT'                                      { return 'INT'; }
+'INTO'                                     { return 'INTO'; }
+'IS'                                       { return 'IS'; }
+'JOIN'                                     { return 'JOIN'; }
+'LEFT'                                     { return 'LEFT'; }
+'LIKE'                                     { return 'LIKE'; }
+'LIMIT'                                    { return 'LIMIT'; }
+'NOT'                                      { return 'NOT'; }
+'NULL'                                     { return 'NULL'; }
+'ON'                                       { return 'ON'; }
+'OPTION'                                   { return 'OPTION'; }
+'OR'                                       { return 'OR'; }
+'ORDER'                                    { return 'ORDER'; }
+'OUTER'                                    { return 'OUTER'; }
+'PARTITION'                                { return 'PARTITION'; }
+'PRECEDING'                                { return 'PRECEDING'; }
+'PURGE'                                    { return 'PURGE'; }
+'RANGE'                                    { return 'RANGE'; }
+'REGEXP'                                   { return 'REGEXP'; }
+'RIGHT'                                    { return 'RIGHT'; }
+'RLIKE'                                    { return 'RLIKE'; }
+'ROW'                                      { return 'ROW'; }
+'ROLE'                                     { return 'ROLE'; }
+'ROWS'                                     { return 'ROWS'; }
+'SCHEMA'                                   { return 'SCHEMA'; }
+'SCHEMAS'                                  { return 'SCHEMAS'; }
+'SELECT'                                   { parser.determineCase(yytext); parser.addStatementTypeLocation('SELECT', yylloc); return 'SELECT'; }
+'SEMI'                                     { return 'SEMI'; }
+'SET'                                      { parser.determineCase(yytext); parser.addStatementTypeLocation('SET', yylloc); return 'SET'; }
+'SHOW'                                     { parser.determineCase(yytext); parser.addStatementTypeLocation('SHOW', yylloc); return 'SHOW'; }
+'SMALLINT'                                 { return 'SMALLINT'; }
+'STRING'                                   { return 'STRING'; }
+'TABLE'                                    { return 'TABLE'; }
+'TABLES'                                   { return 'TABLES'; }
+'THEN'                                     { return 'THEN'; }
+'TIMESTAMP'                                { return 'TIMESTAMP'; }
+'TINYINT'                                  { return 'TINYINT'; }
+'TO'                                       { return 'TO'; }
+'TRUE'                                     { return 'TRUE'; }
+'TRUNCATE'                                 { parser.determineCase(yytext); parser.addStatementTypeLocation('TRUNCATE', yylloc, yy.lexer.upcomingInput()); return 'TRUNCATE'; }
+'UNBOUNDED'                                { return 'UNBOUNDED'; }
+'UNION'                                    { return 'UNION'; }
+'UPDATE'                                   { parser.determineCase(yytext); return 'UPDATE'; }
+'USE'                                      { parser.determineCase(yytext); parser.addStatementTypeLocation('USE', yylloc); return 'USE'; }
+'VALUES'                                   { return 'VALUES'; }
+'VARCHAR'                                  { return 'VARCHAR'; }
+'VIEW'                                     { return 'VIEW'; }
+'WHEN'                                     { return 'WHEN'; }
+'WHERE'                                    { return 'WHERE'; }
+'WITH'                                     { parser.determineCase(yytext); parser.addStatementTypeLocation('WITH', yylloc); return 'WITH'; }
+
+// Non-reserved Keywords
+'OVER'                                     { return 'OVER'; }
+'ROLE'                                     { return 'ROLE'; }
+
+// --- UDFs ---
+AVG\s*\(                                   { yy.lexer.unput('('); yytext = 'avg'; parser.addFunctionLocation(yylloc, yytext); return 'AVG'; }
+CAST\s*\(                                  { yy.lexer.unput('('); yytext = 'cast'; parser.addFunctionLocation(yylloc, yytext); return 'CAST'; }
+COUNT\s*\(                                 { yy.lexer.unput('('); yytext = 'count'; parser.addFunctionLocation(yylloc, yytext); return 'COUNT'; }
+MAX\s*\(                                   { yy.lexer.unput('('); yytext = 'max'; parser.addFunctionLocation(yylloc, yytext); return 'MAX'; }
+MIN\s*\(                                   { yy.lexer.unput('('); yytext = 'min'; parser.addFunctionLocation(yylloc, yytext); return 'MIN'; }
+STDDEV_POP\s*\(                            { yy.lexer.unput('('); yytext = 'stddev_pop'; parser.addFunctionLocation(yylloc, yytext); return 'STDDEV_POP'; }
+STDDEV_SAMP\s*\(                           { yy.lexer.unput('('); yytext = 'stddev_samp'; parser.addFunctionLocation(yylloc, yytext); return 'STDDEV_SAMP'; }
+SUM\s*\(                                   { yy.lexer.unput('('); yytext = 'sum'; parser.addFunctionLocation(yylloc, yytext); return 'SUM'; }
+VAR_POP\s*\(                               { yy.lexer.unput('('); yytext = 'var_pop'; parser.addFunctionLocation(yylloc, yytext); return 'VAR_POP'; }
+VAR_SAMP\s*\(                              { yy.lexer.unput('('); yytext = 'var_samp'; parser.addFunctionLocation(yylloc, yytext); return 'VAR_SAMP'; }
+VARIANCE\s*\(                              { yy.lexer.unput('('); yytext = 'variance'; parser.addFunctionLocation(yylloc, yytext); return 'VARIANCE'; }
+
+// Analytical functions
+CUME_DIST\s*\(                             { yy.lexer.unput('('); yytext = 'cume_dist'; parser.addFunctionLocation(yylloc, yytext); return 'ANALYTIC'; }
+DENSE_RANK\s*\(                            { yy.lexer.unput('('); yytext = 'dense_rank'; parser.addFunctionLocation(yylloc, yytext); return 'ANALYTIC'; }
+FIRST_VALUE\s*\(                           { yy.lexer.unput('('); yytext = 'first_value'; parser.addFunctionLocation(yylloc, yytext); return 'ANALYTIC'; }
+LAG\s*\(                                   { yy.lexer.unput('('); yytext = 'lag'; parser.addFunctionLocation(yylloc, yytext); return 'ANALYTIC'; }
+LAST_VALUE\s*\(                            { yy.lexer.unput('('); yytext = 'last_value'; parser.addFunctionLocation(yylloc, yytext); return 'ANALYTIC'; }
+LEAD\s*\(                                  { yy.lexer.unput('('); yytext = 'lead'; parser.addFunctionLocation(yylloc, yytext); return 'ANALYTIC'; }
+RANK\s*\(                                  { yy.lexer.unput('('); yytext = 'rank'; parser.addFunctionLocation(yylloc, yytext); return 'ANALYTIC'; }
+ROW_NUMBER\s*\(                            { yy.lexer.unput('('); yytext = 'row_number'; parser.addFunctionLocation(yylloc, yytext); return 'ANALYTIC'; }
+
+[0-9]+                                     { return 'UNSIGNED_INTEGER'; }
+[0-9]+(?:[YSL]|BD)?                        { return 'UNSIGNED_INTEGER'; }
+[0-9]+E                                    { return 'UNSIGNED_INTEGER_E'; }
+[A-Za-z0-9_]+                              { return 'REGULAR_IDENTIFIER'; }
+
+<hdfs>'\u2020'                             { parser.yy.cursorFound = true; return 'CURSOR'; }
+<hdfs>'\u2021'                             { parser.yy.cursorFound = true; return 'PARTIAL_CURSOR'; }
+<hdfs>\s+['"]                              { return 'HDFS_START_QUOTE'; }
+<hdfs>[^'"\u2020\u2021]+                   { parser.addFileLocation(yylloc, yytext); return 'HDFS_PATH'; }
+<hdfs>['"]                                 { this.popState(); return 'HDFS_END_QUOTE'; }
+<hdfs><<EOF>>                              { return 'EOF'; }
+
+'&&'                                       { return 'AND'; }
+'||'                                       { return 'OR'; }
+
+'='                                        { return '='; }
+'<'                                        { return '<'; }
+'>'                                        { return '>'; }
+'!='                                       { return 'COMPARISON_OPERATOR'; }
+'<='                                       { return 'COMPARISON_OPERATOR'; }
+'>='                                       { return 'COMPARISON_OPERATOR'; }
+'<>'                                       { return 'COMPARISON_OPERATOR'; }
+'<=>'                                      { return 'COMPARISON_OPERATOR'; }
+
+'-'                                        { return '-'; }
+'*'                                        { return '*'; }
+'+'                                        { return 'ARITHMETIC_OPERATOR'; }
+'/'                                        { return 'ARITHMETIC_OPERATOR'; }
+'%'                                        { return 'ARITHMETIC_OPERATOR'; }
+'|'                                        { return 'ARITHMETIC_OPERATOR'; }
+'^'                                        { return 'ARITHMETIC_OPERATOR'; }
+'&'                                        { return 'ARITHMETIC_OPERATOR'; }
+
+','                                        { return ','; }
+'.'                                        { return '.'; }
+':'                                        { return ':'; }
+';'                                        { return ';'; }
+'~'                                        { return '~'; }
+'!'                                        { return '!'; }
+
+'('                                        { return '('; }
+')'                                        { return ')'; }
+'['                                        { return '['; }
+']'                                        { return ']'; }
+
+\$\{[^}]*\}                                { return 'VARIABLE_REFERENCE'; }
+
+\`                                         { this.begin('backtickedValue'); return 'BACKTICK'; }
+<backtickedValue>[^`]+                     {
+                                             if (parser.handleQuotedValueWithCursor(this, yytext, yylloc, '`')) {
+                                               return 'PARTIAL_VALUE';
+                                             }
+                                             return 'VALUE';
+                                           }
+<backtickedValue>\`                        { this.popState(); return 'BACKTICK'; }
+
+\'                                         { this.begin('singleQuotedValue'); return 'SINGLE_QUOTE'; }
+<singleQuotedValue>(?:\\\\|\\[']|[^'])+         {
+                                             if (parser.handleQuotedValueWithCursor(this, yytext, yylloc, '\'')) {
+                                               return 'PARTIAL_VALUE';
+                                             }
+                                             return 'VALUE';
+                                           }
+<singleQuotedValue>\'                      { this.popState(); return 'SINGLE_QUOTE'; }
+
+\"                                         { this.begin('doubleQuotedValue'); return 'DOUBLE_QUOTE'; }
+<doubleQuotedValue>(?:\\\\|\\["]|[^"])+         {
+                                             if (parser.handleQuotedValueWithCursor(this, yytext, yylloc, '"')) {
+                                               return 'PARTIAL_VALUE';
+                                             }
+                                             return 'VALUE';
+                                           }
+<doubleQuotedValue>\"                      { this.popState(); return 'DOUBLE_QUOTE'; }
+
+<<EOF>>                                    { return 'EOF'; }
+
+.                                          { /* To prevent console logging of unknown chars */ }
+<between>.                                 { }
+<hdfs>.                                    { }
+<backtickedValue>.                         { }
+<singleQuotedValue>.                       { }
+<doubleQuotedValue>.                       { }

+ 74 - 0
desktop/core/src/desktop/js/parse/sql/sparksql/jison/structure.json

@@ -0,0 +1,74 @@
+{
+  "lexer": "sql.jisonlex",
+  "autocomplete": [
+    "../../generic/jison/autocomplete_header.jison",
+    "../../generic/jison/select/cte_select_statement.jison",
+    "../../generic/jison/select/from_clause.jison",
+    "../../generic/jison/select/group_by_clause.jison",
+    "../../generic/jison/select/having_clause.jison",
+    "../../generic/jison/select/joins.jison",
+    "../../generic/jison/select/limit_clause.jison",
+    "../../generic/jison/select/order_by_clause.jison",
+    "../../generic/jison/select/select.jison",
+    "../../generic/jison/select/select_conditions.jison",
+    "../../generic/jison/select/union_clause.jison",
+    "../../generic/jison/select/where_clause.jison",
+    "../../generic/jison/udf/aggregate/aggregate_common.jison",
+    "../../generic/jison/udf/aggregate/avg.jison",
+    "../../generic/jison/udf/aggregate/count.jison",
+    "../../generic/jison/udf/aggregate/max.jison",
+    "../../generic/jison/udf/aggregate/min.jison",
+    "../../generic/jison/udf/aggregate/stddev_pop.jison",
+    "../../generic/jison/udf/aggregate/stddev_samp.jison",
+    "../../generic/jison/udf/aggregate/sum.jison",
+    "../../generic/jison/udf/aggregate/var_pop.jison",
+    "../../generic/jison/udf/aggregate/var_samp.jison",
+    "../../generic/jison/udf/aggregate/variance.jison",
+    "../../generic/jison/udf/analytic/analytic.jison",
+    "../../generic/jison/udf/function/array.jison",
+    "../../generic/jison/udf/function/cast.jison",
+    "../../generic/jison/udf/function/if.jison",
+    "../../generic/jison/udf/function/map.jison",
+    "../../generic/jison/udf/function/truncate.jison",
+    "../../generic/jison/udf/udf_common.jison",
+    "../../generic/jison/sql_error.jison",
+    "../../generic/jison/sql_main.jison",
+    "../../generic/jison/sql_valueExpression.jison",
+    "../../generic/jison/autocomplete_footer.jison"
+  ],
+  "syntax": [
+    "../../generic/jison/syntax_header.jison",
+    "../../generic/jison/select/cte_select_statement.jison",
+    "../../generic/jison/select/from_clause.jison",
+    "../../generic/jison/select/group_by_clause.jison",
+    "../../generic/jison/select/having_clause.jison",
+    "../../generic/jison/select/joins.jison",
+    "../../generic/jison/select/limit_clause.jison",
+    "../../generic/jison/select/order_by_clause.jison",
+    "../../generic/jison/select/select.jison",
+    "../../generic/jison/select/select_conditions.jison",
+    "../../generic/jison/select/union_clause.jison",
+    "../../generic/jison/select/where_clause.jison",
+    "../../generic/jison/udf/aggregate/aggregate_common.jison",
+    "../../generic/jison/udf/aggregate/avg.jison",
+    "../../generic/jison/udf/aggregate/count.jison",
+    "../../generic/jison/udf/aggregate/max.jison",
+    "../../generic/jison/udf/aggregate/min.jison",
+    "../../generic/jison/udf/aggregate/stddev_pop.jison",
+    "../../generic/jison/udf/aggregate/stddev_samp.jison",
+    "../../generic/jison/udf/aggregate/sum.jison",
+    "../../generic/jison/udf/aggregate/var_pop.jison",
+    "../../generic/jison/udf/aggregate/var_samp.jison",
+    "../../generic/jison/udf/aggregate/variance.jison",
+    "../../generic/jison/udf/analytic/analytic.jison",
+    "../../generic/jison/udf/function/array.jison",
+    "../../generic/jison/udf/function/cast.jison",
+    "../../generic/jison/udf/function/if.jison",
+    "../../generic/jison/udf/function/map.jison",
+    "../../generic/jison/udf/function/truncate.jison",
+    "../../generic/jison/udf/udf_common.jison",
+    "../../generic/jison/sql_main.jison",
+    "../../generic/jison/sql_valueExpression.jison",
+    "../../generic/jison/syntax_footer.jison"
+  ]
+}

+ 1326 - 0
desktop/core/src/desktop/js/parse/sql/sparksql/sparksqlAutocompleteParser.Locations.test.js

@@ -0,0 +1,1326 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// 'License'); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import sparksqlAutocompleteParser from './sparksqlAutocompleteParser';
+
+// prettier-ignore-start
+describe('sparksqlAutocompleteParser.js locations', () => {
+  beforeAll(() => {
+    sparksqlAutocompleteParser.yy.parseError = function (msg) {
+      throw Error(msg);
+    };
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = false;
+    expect(
+      sparksqlAutocompleteParser.parseSql(
+        testDefinition.beforeCursor,
+        testDefinition.afterCursor,
+        debug
+      )
+    ).toEqualDefinition(testDefinition);
+  };
+
+  const assertLocations = function (options) {
+    assertAutoComplete({
+      beforeCursor: options.beforeCursor,
+      afterCursor: options.afterCursor || '',
+      locationsOnly: true,
+      noErrors: true,
+      expectedLocations: options.expectedLocations,
+      expectedDefinitions: options.expectedDefinitions
+    });
+  };
+
+  it('should report locations for "select cos(1) as foo from customers order by foo;"', () => {
+    assertLocations({
+      beforeCursor: 'select cos(1) as foo from customers order by foo; ',
+      expectedLocations: [
+        {
+          type: 'statement',
+          location: { first_line: 1, last_line: 1, first_column: 1, last_column: 49 }
+        },
+        {
+          type: 'selectList',
+          missing: false,
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 21 }
+        },
+        {
+          type: 'function',
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 10 },
+          function: 'cos'
+        },
+        {
+          type: 'functionArgument',
+          location: { first_line: 1, last_line: 1, first_column: 12, last_column: 13 },
+          function: 'cos',
+          argumentPosition: 0,
+          identifierChain: [{ name: 'cos' }],
+          expression: { types: ['NUMBER'], text: '1' }
+        },
+        {
+          type: 'alias',
+          source: 'column',
+          alias: 'foo',
+          location: { first_line: 1, last_line: 1, first_column: 18, last_column: 21 },
+          parentLocation: { first_line: 1, last_line: 1, first_column: 8, last_column: 14 }
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 27, last_column: 36 },
+          identifierChain: [{ name: 'customers' }]
+        },
+        {
+          type: 'whereClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 36, last_column: 36 }
+        },
+        {
+          type: 'alias',
+          location: { first_line: 1, last_line: 1, first_column: 46, last_column: 49 },
+          alias: 'foo',
+          source: 'column',
+          parentLocation: { first_line: 1, last_line: 1, first_column: 8, last_column: 14 }
+        },
+        {
+          type: 'limitClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 49, last_column: 49 }
+        }
+      ]
+    });
+  });
+
+  it('should report locations for "WITH boo AS (SELECT * FROM tbl) SELECT * FROM boo; |"', () => {
+    assertLocations({
+      beforeCursor: 'WITH boo AS (SELECT * FROM tbl) SELECT * FROM boo; ',
+      expectedLocations: [
+        {
+          type: 'statement',
+          location: { first_line: 1, last_line: 1, first_column: 1, last_column: 50 }
+        },
+        {
+          type: 'alias',
+          source: 'cte',
+          alias: 'boo',
+          location: { first_line: 1, last_line: 1, first_column: 6, last_column: 9 }
+        },
+        {
+          type: 'selectList',
+          missing: false,
+          location: { first_line: 1, last_line: 1, first_column: 21, last_column: 22 },
+          subquery: true
+        },
+        {
+          type: 'asterisk',
+          location: { first_line: 1, last_line: 1, first_column: 21, last_column: 22 },
+          tables: [{ identifierChain: [{ name: 'tbl' }] }]
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 28, last_column: 31 },
+          identifierChain: [{ name: 'tbl' }]
+        },
+        {
+          type: 'whereClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 31, last_column: 31 },
+          subquery: true
+        },
+        {
+          type: 'limitClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 31, last_column: 31 },
+          subquery: true
+        },
+        {
+          type: 'selectList',
+          missing: false,
+          location: { first_line: 1, last_line: 1, first_column: 40, last_column: 41 }
+        },
+        {
+          type: 'asterisk',
+          location: { first_line: 1, last_line: 1, first_column: 40, last_column: 41 },
+          tables: [{ identifierChain: [{ name: 'boo' }] }]
+        },
+        {
+          type: 'alias',
+          target: 'cte',
+          alias: 'boo',
+          location: { first_line: 1, last_line: 1, first_column: 47, last_column: 50 }
+        },
+        {
+          type: 'whereClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 50, last_column: 50 }
+        },
+        {
+          type: 'limitClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 50, last_column: 50 }
+        }
+      ]
+    });
+  });
+
+  it('should report locations for "SELECT * FROM testTable1 JOIN db1.table2; |"', () => {
+    assertLocations({
+      beforeCursor: 'SELECT * FROM testTable1 JOIN db1.table2; ',
+      expectedLocations: [
+        {
+          type: 'statement',
+          location: { first_line: 1, last_line: 1, first_column: 1, last_column: 41 }
+        },
+        {
+          type: 'selectList',
+          missing: false,
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 }
+        },
+        {
+          type: 'asterisk',
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 },
+          tables: [
+            { identifierChain: [{ name: 'testTable1' }] },
+            { identifierChain: [{ name: 'db1' }, { name: 'table2' }] }
+          ]
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 15, last_column: 25 },
+          identifierChain: [{ name: 'testTable1' }]
+        },
+        {
+          type: 'database',
+          location: { first_line: 1, last_line: 1, first_column: 31, last_column: 34 },
+          identifierChain: [{ name: 'db1' }]
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 35, last_column: 41 },
+          identifierChain: [{ name: 'db1' }, { name: 'table2' }]
+        },
+        {
+          type: 'whereClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 41, last_column: 41 }
+        },
+        {
+          type: 'limitClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 41, last_column: 41 }
+        }
+      ]
+    });
+  });
+
+  it('should report locations for "SELECT a.col FROM db.tbl a; |"', () => {
+    assertLocations({
+      beforeCursor: 'SELECT a.col FROM db.tbl a; ',
+      expectedLocations: [
+        {
+          type: 'statement',
+          location: { first_line: 1, last_line: 1, first_column: 1, last_column: 27 }
+        },
+        {
+          type: 'selectList',
+          missing: false,
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 13 }
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 },
+          identifierChain: [{ name: 'db' }, { name: 'tbl' }]
+        },
+        {
+          type: 'column',
+          location: { first_line: 1, last_line: 1, first_column: 10, last_column: 13 },
+          identifierChain: [{ name: 'col' }],
+          tables: [{ identifierChain: [{ name: 'db' }, { name: 'tbl' }], alias: 'a' }],
+          qualified: true
+        },
+        {
+          type: 'database',
+          location: { first_line: 1, last_line: 1, first_column: 19, last_column: 21 },
+          identifierChain: [{ name: 'db' }]
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 22, last_column: 25 },
+          identifierChain: [{ name: 'db' }, { name: 'tbl' }]
+        },
+        {
+          type: 'alias',
+          source: 'table',
+          alias: 'a',
+          location: { first_line: 1, last_line: 1, first_column: 26, last_column: 27 },
+          identifierChain: [{ name: 'db' }, { name: 'tbl' }]
+        },
+        {
+          type: 'whereClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 27, last_column: 27 }
+        },
+        {
+          type: 'limitClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 27, last_column: 27 }
+        }
+      ]
+    });
+  });
+
+  it('should report locations for "SELECT tbl.col FROM db.tbl a; |"', () => {
+    assertLocations({
+      beforeCursor: 'SELECT tbl.col FROM db.tbl a; ',
+      expectedLocations: [
+        {
+          type: 'statement',
+          location: { first_line: 1, last_line: 1, first_column: 1, last_column: 29 }
+        },
+        {
+          type: 'selectList',
+          missing: false,
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 15 }
+        },
+        {
+          type: 'column',
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 11 },
+          identifierChain: [{ name: 'tbl' }],
+          tables: [{ identifierChain: [{ name: 'db' }, { name: 'tbl' }], alias: 'a' }],
+          qualified: false
+        },
+        {
+          type: 'complex',
+          location: { first_line: 1, last_line: 1, first_column: 12, last_column: 15 },
+          identifierChain: [{ name: 'tbl' }, { name: 'col' }],
+          tables: [{ identifierChain: [{ name: 'db' }, { name: 'tbl' }], alias: 'a' }],
+          qualified: true
+        },
+        {
+          type: 'database',
+          location: { first_line: 1, last_line: 1, first_column: 21, last_column: 23 },
+          identifierChain: [{ name: 'db' }]
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 24, last_column: 27 },
+          identifierChain: [{ name: 'db' }, { name: 'tbl' }]
+        },
+        {
+          type: 'alias',
+          source: 'table',
+          alias: 'a',
+          location: { first_line: 1, last_line: 1, first_column: 28, last_column: 29 },
+          identifierChain: [{ name: 'db' }, { name: 'tbl' }]
+        },
+        {
+          type: 'whereClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 29, last_column: 29 }
+        },
+        {
+          type: 'limitClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 29, last_column: 29 }
+        }
+      ]
+    });
+  });
+
+  it('should report locations for "select x from x;"', () => {
+    assertLocations({
+      beforeCursor: 'select x from x;',
+      expectedLocations: [
+        {
+          type: 'statement',
+          location: { first_line: 1, last_line: 1, first_column: 1, last_column: 16 }
+        },
+        {
+          type: 'selectList',
+          missing: false,
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 }
+        },
+        {
+          type: 'column',
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 },
+          identifierChain: [{ name: 'x' }],
+          tables: [{ identifierChain: [{ name: 'x' }] }],
+          qualified: false
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 15, last_column: 16 },
+          identifierChain: [{ name: 'x' }]
+        },
+        {
+          type: 'whereClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 16, last_column: 16 }
+        },
+        {
+          type: 'limitClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 16, last_column: 16 }
+        }
+      ]
+    });
+  });
+
+  it('should report locations for "select x from x;select y from y;"', () => {
+    assertLocations({
+      beforeCursor: 'select x from x;select y from y;',
+      expectedLocations: [
+        {
+          type: 'statement',
+          location: { first_line: 1, last_line: 1, first_column: 1, last_column: 16 }
+        },
+        {
+          type: 'selectList',
+          missing: false,
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 }
+        },
+        {
+          type: 'column',
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 },
+          identifierChain: [{ name: 'x' }],
+          tables: [{ identifierChain: [{ name: 'x' }] }],
+          qualified: false
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 15, last_column: 16 },
+          identifierChain: [{ name: 'x' }]
+        },
+        {
+          type: 'whereClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 16, last_column: 16 }
+        },
+        {
+          type: 'limitClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 16, last_column: 16 }
+        },
+        {
+          type: 'statement',
+          location: { first_line: 1, last_line: 1, first_column: 17, last_column: 32 }
+        },
+        {
+          type: 'selectList',
+          missing: false,
+          location: { first_line: 1, last_line: 1, first_column: 24, last_column: 25 }
+        },
+        {
+          type: 'column',
+          location: { first_line: 1, last_line: 1, first_column: 24, last_column: 25 },
+          identifierChain: [{ name: 'y' }],
+          tables: [{ identifierChain: [{ name: 'y' }] }],
+          qualified: false
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 31, last_column: 32 },
+          identifierChain: [{ name: 'y' }]
+        },
+        {
+          type: 'whereClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 32, last_column: 32 }
+        },
+        {
+          type: 'limitClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 32, last_column: 32 }
+        }
+      ]
+    });
+  });
+
+  it('should report locations for "-- comment\nselect x from x;\n\n\nselect y from y;"', () => {
+    assertLocations({
+      beforeCursor: '-- comment\nselect x from x;\n\n\nselect y from y;',
+      expectedLocations: [
+        {
+          type: 'statement',
+          location: { first_line: 1, last_line: 2, first_column: 1, last_column: 16 }
+        },
+        {
+          type: 'selectList',
+          missing: false,
+          location: { first_line: 2, last_line: 2, first_column: 8, last_column: 9 }
+        },
+        {
+          type: 'column',
+          location: { first_line: 2, last_line: 2, first_column: 8, last_column: 9 },
+          identifierChain: [{ name: 'x' }],
+          tables: [{ identifierChain: [{ name: 'x' }] }],
+          qualified: false
+        },
+        {
+          type: 'table',
+          location: { first_line: 2, last_line: 2, first_column: 15, last_column: 16 },
+          identifierChain: [{ name: 'x' }]
+        },
+        {
+          type: 'whereClause',
+          missing: true,
+          location: { first_line: 2, last_line: 2, first_column: 16, last_column: 16 }
+        },
+        {
+          type: 'limitClause',
+          missing: true,
+          location: { first_line: 2, last_line: 2, first_column: 16, last_column: 16 }
+        },
+        {
+          type: 'statement',
+          location: { first_line: 2, last_line: 5, first_column: 17, last_column: 16 }
+        },
+        {
+          type: 'selectList',
+          missing: false,
+          location: { first_line: 5, last_line: 5, first_column: 8, last_column: 9 }
+        },
+        {
+          type: 'column',
+          location: { first_line: 5, last_line: 5, first_column: 8, last_column: 9 },
+          identifierChain: [{ name: 'y' }],
+          tables: [{ identifierChain: [{ name: 'y' }] }],
+          qualified: false
+        },
+        {
+          type: 'table',
+          location: { first_line: 5, last_line: 5, first_column: 15, last_column: 16 },
+          identifierChain: [{ name: 'y' }]
+        },
+        {
+          type: 'whereClause',
+          missing: true,
+          location: { first_line: 5, last_line: 5, first_column: 16, last_column: 16 }
+        },
+        {
+          type: 'limitClause',
+          missing: true,
+          location: { first_line: 5, last_line: 5, first_column: 16, last_column: 16 }
+        }
+      ]
+    });
+  });
+
+  it('should report locations for "select x from x, y;"', () => {
+    assertLocations({
+      beforeCursor: 'select x from x, y;',
+      expectedLocations: [
+        {
+          type: 'statement',
+          location: { first_line: 1, last_line: 1, first_column: 1, last_column: 19 }
+        },
+        {
+          type: 'selectList',
+          missing: false,
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 }
+        },
+        {
+          type: 'column',
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 },
+          identifierChain: [{ name: 'x' }],
+          tables: [{ identifierChain: [{ name: 'x' }] }, { identifierChain: [{ name: 'y' }] }],
+          qualified: false
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 15, last_column: 16 },
+          identifierChain: [{ name: 'x' }]
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 18, last_column: 19 },
+          identifierChain: [{ name: 'y' }]
+        },
+        {
+          type: 'whereClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 19, last_column: 19 }
+        },
+        {
+          type: 'limitClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 19, last_column: 19 }
+        }
+      ]
+    });
+  });
+
+  it('should report locations for "SELECT t3.id, id FROM testTable1, db.testTable2, testTable3 t3;|"', () => {
+    assertLocations({
+      beforeCursor: 'SELECT t3.id, id FROM testTable1, db.testTable2, testTable3 t3;',
+      expectedLocations: [
+        {
+          type: 'statement',
+          location: { first_line: 1, last_line: 1, first_column: 1, last_column: 63 }
+        },
+        {
+          type: 'selectList',
+          missing: false,
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 17 }
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 10 },
+          identifierChain: [{ name: 'testTable3' }]
+        },
+        {
+          type: 'column',
+          location: { first_line: 1, last_line: 1, first_column: 11, last_column: 13 },
+          identifierChain: [{ name: 'id' }],
+          tables: [{ identifierChain: [{ name: 'testTable3' }], alias: 't3' }],
+          qualified: true
+        },
+        {
+          type: 'column',
+          location: { first_line: 1, last_line: 1, first_column: 15, last_column: 17 },
+          identifierChain: [{ name: 'id' }],
+          tables: [
+            { identifierChain: [{ name: 'testTable1' }] },
+            { identifierChain: [{ name: 'db' }, { name: 'testTable2' }] },
+            { identifierChain: [{ name: 'testTable3' }], alias: 't3' }
+          ],
+          qualified: false
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 23, last_column: 33 },
+          identifierChain: [{ name: 'testTable1' }]
+        },
+        {
+          type: 'database',
+          location: { first_line: 1, last_line: 1, first_column: 35, last_column: 37 },
+          identifierChain: [{ name: 'db' }]
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 38, last_column: 48 },
+          identifierChain: [{ name: 'db' }, { name: 'testTable2' }]
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 50, last_column: 60 },
+          identifierChain: [{ name: 'testTable3' }]
+        },
+        {
+          type: 'alias',
+          source: 'table',
+          alias: 't3',
+          location: { first_line: 1, last_line: 1, first_column: 61, last_column: 63 },
+          identifierChain: [{ name: 'testTable3' }]
+        },
+        {
+          type: 'whereClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 63, last_column: 63 }
+        },
+        {
+          type: 'limitClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 63, last_column: 63 }
+        }
+      ]
+    });
+  });
+
+  it('should report locations for "SELECT * FROM foo WHERE bar IN (1+1, 2+2);|"', () => {
+    assertLocations({
+      beforeCursor: 'SELECT * FROM foo WHERE bar IN (1+1, 2+2);',
+      expectedLocations: [
+        {
+          type: 'statement',
+          location: { first_line: 1, last_line: 1, first_column: 1, last_column: 42 }
+        },
+        {
+          type: 'selectList',
+          missing: false,
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 }
+        },
+        {
+          type: 'asterisk',
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 },
+          tables: [{ identifierChain: [{ name: 'foo' }] }]
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 15, last_column: 18 },
+          identifierChain: [{ name: 'foo' }]
+        },
+        {
+          type: 'whereClause',
+          missing: false,
+          location: { first_line: 1, last_line: 1, first_column: 19, last_column: 42 }
+        },
+        {
+          type: 'column',
+          location: { first_line: 1, last_line: 1, first_column: 25, last_column: 28 },
+          identifierChain: [{ name: 'bar' }],
+          tables: [{ identifierChain: [{ name: 'foo' }] }],
+          qualified: false
+        },
+        {
+          type: 'limitClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 42, last_column: 42 }
+        }
+      ]
+    });
+  });
+
+  it('should report locations for "SELECT * FROM foo WHERE bar IN (id+1-1, id+1-2);|"', () => {
+    assertLocations({
+      beforeCursor: 'SELECT * FROM foo WHERE bar IN (id+1-1, id+1-2);',
+      expectedLocations: [
+        {
+          type: 'statement',
+          location: { first_line: 1, last_line: 1, first_column: 1, last_column: 48 }
+        },
+        {
+          type: 'selectList',
+          missing: false,
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 }
+        },
+        {
+          type: 'asterisk',
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 },
+          tables: [{ identifierChain: [{ name: 'foo' }] }]
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 15, last_column: 18 },
+          identifierChain: [{ name: 'foo' }]
+        },
+        {
+          type: 'whereClause',
+          missing: false,
+          location: { first_line: 1, last_line: 1, first_column: 19, last_column: 48 }
+        },
+        {
+          type: 'column',
+          location: { first_line: 1, last_line: 1, first_column: 25, last_column: 28 },
+          identifierChain: [{ name: 'bar' }],
+          tables: [{ identifierChain: [{ name: 'foo' }] }],
+          qualified: false
+        },
+        {
+          type: 'column',
+          location: { first_line: 1, last_line: 1, first_column: 33, last_column: 35 },
+          identifierChain: [{ name: 'id' }],
+          tables: [{ identifierChain: [{ name: 'foo' }] }],
+          qualified: false
+        },
+        {
+          type: 'column',
+          location: { first_line: 1, last_line: 1, first_column: 41, last_column: 43 },
+          identifierChain: [{ name: 'id' }],
+          tables: [{ identifierChain: [{ name: 'foo' }] }],
+          qualified: false
+        },
+        {
+          type: 'limitClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 48, last_column: 48 }
+        }
+      ]
+    });
+  });
+
+  it(
+    'should report locations for "SELECT s07.description, s07.salary, s08.salary,\\r\\n' +
+      '  s08.salary - s07.salary\\r\\n' +
+      'FROM\\r\\n' +
+      '  sample_07 s07 JOIN sample_08 s08\\r\\n' +
+      'ON ( s07.code = s08.code)\\r\\n' +
+      'WHERE\\r\\n' +
+      '  s07.salary < s08.salary\\r\\n' +
+      'ORDER BY s08.salary-s07.salary DESC\\r\\n' +
+      'LIMIT 1000;|"',
+    () => {
+      assertLocations({
+        beforeCursor:
+          'SELECT s07.description, s07.salary, s08.salary,\r\n' +
+          '  s08.salary - s07.salary\r\n' +
+          'FROM\r\n' +
+          '  sample_07 s07 JOIN sample_08 s08\r\n' +
+          'ON ( s07.code = s08.code)\r\n' +
+          'WHERE\r\n' +
+          '  s07.salary < s08.salary\r\n' +
+          'ORDER BY s08.salary-s07.salary DESC\r\n' +
+          'LIMIT 1000;',
+        expectedLocations: [
+          {
+            type: 'statement',
+            location: { first_line: 1, last_line: 9, first_column: 1, last_column: 11 }
+          },
+          {
+            type: 'selectList',
+            missing: false,
+            location: { first_line: 1, last_line: 2, first_column: 8, last_column: 26 }
+          },
+          {
+            type: 'table',
+            location: { first_line: 1, last_line: 1, first_column: 8, last_column: 11 },
+            identifierChain: [{ name: 'sample_07' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 12, last_column: 23 },
+            identifierChain: [{ name: 'description' }],
+            tables: [{ identifierChain: [{ name: 'sample_07' }], alias: 's07' }],
+            qualified: true
+          },
+          {
+            type: 'table',
+            location: { first_line: 1, last_line: 1, first_column: 25, last_column: 28 },
+            identifierChain: [{ name: 'sample_07' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 29, last_column: 35 },
+            identifierChain: [{ name: 'salary' }],
+            tables: [{ identifierChain: [{ name: 'sample_07' }], alias: 's07' }],
+            qualified: true
+          },
+          {
+            type: 'table',
+            location: { first_line: 1, last_line: 1, first_column: 37, last_column: 40 },
+            identifierChain: [{ name: 'sample_08' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 41, last_column: 47 },
+            identifierChain: [{ name: 'salary' }],
+            tables: [{ identifierChain: [{ name: 'sample_08' }], alias: 's08' }],
+            qualified: true
+          },
+          {
+            type: 'table',
+            location: { first_line: 2, last_line: 2, first_column: 3, last_column: 6 },
+            identifierChain: [{ name: 'sample_08' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 2, last_line: 2, first_column: 7, last_column: 13 },
+            identifierChain: [{ name: 'salary' }],
+            tables: [{ identifierChain: [{ name: 'sample_08' }], alias: 's08' }],
+            qualified: true
+          },
+          {
+            type: 'table',
+            location: { first_line: 2, last_line: 2, first_column: 16, last_column: 19 },
+            identifierChain: [{ name: 'sample_07' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 2, last_line: 2, first_column: 20, last_column: 26 },
+            identifierChain: [{ name: 'salary' }],
+            tables: [{ identifierChain: [{ name: 'sample_07' }], alias: 's07' }],
+            qualified: true
+          },
+          {
+            type: 'table',
+            location: { first_line: 4, last_line: 4, first_column: 3, last_column: 12 },
+            identifierChain: [{ name: 'sample_07' }]
+          },
+          {
+            type: 'alias',
+            source: 'table',
+            alias: 's07',
+            location: { first_line: 4, last_line: 4, first_column: 13, last_column: 16 },
+            identifierChain: [{ name: 'sample_07' }]
+          },
+          {
+            type: 'table',
+            location: { first_line: 4, last_line: 4, first_column: 22, last_column: 31 },
+            identifierChain: [{ name: 'sample_08' }]
+          },
+          {
+            type: 'alias',
+            source: 'table',
+            alias: 's08',
+            location: { first_line: 4, last_line: 4, first_column: 32, last_column: 35 },
+            identifierChain: [{ name: 'sample_08' }]
+          },
+          {
+            type: 'table',
+            location: { first_line: 5, last_line: 5, first_column: 6, last_column: 9 },
+            identifierChain: [{ name: 'sample_07' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 5, last_line: 5, first_column: 10, last_column: 14 },
+            identifierChain: [{ name: 'code' }],
+            tables: [{ identifierChain: [{ name: 'sample_07' }], alias: 's07' }],
+            qualified: true
+          },
+          {
+            type: 'table',
+            location: { first_line: 5, last_line: 5, first_column: 17, last_column: 20 },
+            identifierChain: [{ name: 'sample_08' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 5, last_line: 5, first_column: 21, last_column: 25 },
+            identifierChain: [{ name: 'code' }],
+            tables: [{ identifierChain: [{ name: 'sample_08' }], alias: 's08' }],
+            qualified: true
+          },
+          {
+            type: 'whereClause',
+            missing: false,
+            location: { first_line: 6, last_line: 7, first_column: 1, last_column: 26 }
+          },
+          {
+            type: 'table',
+            location: { first_line: 7, last_line: 7, first_column: 3, last_column: 6 },
+            identifierChain: [{ name: 'sample_07' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 7, last_line: 7, first_column: 7, last_column: 13 },
+            identifierChain: [{ name: 'salary' }],
+            tables: [{ identifierChain: [{ name: 'sample_07' }], alias: 's07' }],
+            qualified: true
+          },
+          {
+            type: 'table',
+            location: { first_line: 7, last_line: 7, first_column: 16, last_column: 19 },
+            identifierChain: [{ name: 'sample_08' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 7, last_line: 7, first_column: 20, last_column: 26 },
+            identifierChain: [{ name: 'salary' }],
+            tables: [{ identifierChain: [{ name: 'sample_08' }], alias: 's08' }],
+            qualified: true
+          },
+          {
+            type: 'table',
+            location: { first_line: 8, last_line: 8, first_column: 10, last_column: 13 },
+            identifierChain: [{ name: 'sample_08' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 8, last_line: 8, first_column: 14, last_column: 20 },
+            identifierChain: [{ name: 'salary' }],
+            tables: [{ identifierChain: [{ name: 'sample_08' }], alias: 's08' }],
+            qualified: true
+          },
+          {
+            type: 'table',
+            location: { first_line: 8, last_line: 8, first_column: 21, last_column: 24 },
+            identifierChain: [{ name: 'sample_07' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 8, last_line: 8, first_column: 25, last_column: 31 },
+            identifierChain: [{ name: 'salary' }],
+            tables: [{ identifierChain: [{ name: 'sample_07' }], alias: 's07' }],
+            qualified: true
+          },
+          {
+            type: 'limitClause',
+            missing: false,
+            location: { first_line: 9, last_line: 9, first_column: 1, last_column: 11 }
+          }
+        ]
+      });
+    }
+  );
+
+  it('should handle "select bl from blablabla join (select * from blablabla) s1;', () => {
+    assertLocations({
+      beforeCursor: 'select bl from blablabla join (select * from blablabla) s1;',
+      afterCursor: '',
+      expectedLocations: [
+        {
+          type: 'statement',
+          location: { first_line: 1, last_line: 1, first_column: 1, last_column: 59 }
+        },
+        {
+          type: 'selectList',
+          missing: false,
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 10 }
+        },
+        {
+          type: 'column',
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 10 },
+          identifierChain: [{ name: 'bl' }],
+          tables: [{ identifierChain: [{ name: 'blablabla' }] }, { subQuery: 's1' }],
+          qualified: false
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 16, last_column: 25 },
+          identifierChain: [{ name: 'blablabla' }]
+        },
+        {
+          type: 'selectList',
+          missing: false,
+          location: { first_line: 1, last_line: 1, first_column: 39, last_column: 40 },
+          subquery: true
+        },
+        {
+          type: 'asterisk',
+          location: { first_line: 1, last_line: 1, first_column: 39, last_column: 40 },
+          tables: [{ identifierChain: [{ name: 'blablabla' }] }]
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 46, last_column: 55 },
+          identifierChain: [{ name: 'blablabla' }]
+        },
+        {
+          type: 'whereClause',
+          subquery: true,
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 55, last_column: 55 }
+        },
+        {
+          type: 'limitClause',
+          subquery: true,
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 55, last_column: 55 }
+        },
+        {
+          type: 'alias',
+          source: 'subquery',
+          alias: 's1',
+          location: { first_line: 1, last_line: 1, first_column: 57, last_column: 59 }
+        },
+        {
+          type: 'whereClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 59, last_column: 59 }
+        },
+        {
+          type: 'limitClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 59, last_column: 59 }
+        }
+      ]
+    });
+  });
+
+  it(
+    'should report locations for "SELECT CASE cos(boo.a) > baa.boo \\n' +
+      '\\tWHEN baa.b THEN true \\n' +
+      '\\tWHEN boo.c THEN false \\n' +
+      '\\tWHEN baa.blue THEN boo.d \\n' +
+      '\\tELSE baa.e END \\n' +
+      '\\t FROM db1.foo boo, bar baa WHERE baa.bla IN (SELECT ble FROM bla);|"',
+    () => {
+      assertLocations({
+        beforeCursor:
+          'SELECT CASE cos(boo.a) > baa.boo \n\tWHEN baa.b THEN true \n\tWHEN boo.c THEN false \n\tWHEN baa.blue THEN boo.d \n\tELSE baa.e END \n\t FROM db1.foo boo, bar baa WHERE baa.bla IN (SELECT ble FROM bla);',
+        expectedLocations: [
+          {
+            type: 'statement',
+            location: { first_line: 1, last_line: 6, first_column: 1, last_column: 67 }
+          },
+          {
+            type: 'selectList',
+            missing: false,
+            location: { first_line: 1, last_line: 5, first_column: 8, last_column: 16 }
+          },
+          {
+            type: 'function',
+            location: { first_line: 1, last_line: 1, first_column: 13, last_column: 15 },
+            function: 'cos'
+          },
+          {
+            type: 'functionArgument',
+            location: { first_line: 1, last_line: 1, first_column: 17, last_column: 22 },
+            function: 'cos',
+            argumentPosition: 0,
+            identifierChain: [{ name: 'cos' }],
+            expression: { types: ['COLREF'], columnReference: [{ name: 'boo' }, { name: 'a' }] }
+          },
+          {
+            type: 'table',
+            location: { first_line: 1, last_line: 1, first_column: 17, last_column: 20 },
+            identifierChain: [{ name: 'db1' }, { name: 'foo' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 21, last_column: 22 },
+            identifierChain: [{ name: 'a' }],
+            tables: [{ identifierChain: [{ name: 'db1' }, { name: 'foo' }], alias: 'boo' }],
+            qualified: true
+          },
+          {
+            type: 'table',
+            location: { first_line: 1, last_line: 1, first_column: 26, last_column: 29 },
+            identifierChain: [{ name: 'bar' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 30, last_column: 33 },
+            identifierChain: [{ name: 'boo' }],
+            tables: [{ identifierChain: [{ name: 'bar' }], alias: 'baa' }],
+            qualified: true
+          },
+          {
+            type: 'table',
+            location: { first_line: 2, last_line: 2, first_column: 7, last_column: 10 },
+            identifierChain: [{ name: 'bar' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 2, last_line: 2, first_column: 11, last_column: 12 },
+            identifierChain: [{ name: 'b' }],
+            tables: [{ identifierChain: [{ name: 'bar' }], alias: 'baa' }],
+            qualified: true
+          },
+          {
+            type: 'table',
+            location: { first_line: 3, last_line: 3, first_column: 7, last_column: 10 },
+            identifierChain: [{ name: 'db1' }, { name: 'foo' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 3, last_line: 3, first_column: 11, last_column: 12 },
+            identifierChain: [{ name: 'c' }],
+            tables: [{ identifierChain: [{ name: 'db1' }, { name: 'foo' }], alias: 'boo' }],
+            qualified: true
+          },
+          {
+            type: 'table',
+            location: { first_line: 4, last_line: 4, first_column: 7, last_column: 10 },
+            identifierChain: [{ name: 'bar' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 4, last_line: 4, first_column: 11, last_column: 15 },
+            identifierChain: [{ name: 'blue' }],
+            tables: [{ identifierChain: [{ name: 'bar' }], alias: 'baa' }],
+            qualified: true
+          },
+          {
+            type: 'table',
+            location: { first_line: 4, last_line: 4, first_column: 21, last_column: 24 },
+            identifierChain: [{ name: 'db1' }, { name: 'foo' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 4, last_line: 4, first_column: 25, last_column: 26 },
+            identifierChain: [{ name: 'd' }],
+            tables: [{ identifierChain: [{ name: 'db1' }, { name: 'foo' }], alias: 'boo' }],
+            qualified: true
+          },
+          {
+            type: 'table',
+            location: { first_line: 5, last_line: 5, first_column: 7, last_column: 10 },
+            identifierChain: [{ name: 'bar' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 5, last_line: 5, first_column: 11, last_column: 12 },
+            identifierChain: [{ name: 'e' }],
+            tables: [{ identifierChain: [{ name: 'bar' }], alias: 'baa' }],
+            qualified: true
+          },
+          {
+            type: 'database',
+            location: { first_line: 6, last_line: 6, first_column: 8, last_column: 11 },
+            identifierChain: [{ name: 'db1' }]
+          },
+          {
+            type: 'table',
+            location: { first_line: 6, last_line: 6, first_column: 12, last_column: 15 },
+            identifierChain: [{ name: 'db1' }, { name: 'foo' }]
+          },
+          {
+            type: 'alias',
+            source: 'table',
+            alias: 'boo',
+            location: { first_line: 6, last_line: 6, first_column: 16, last_column: 19 },
+            identifierChain: [{ name: 'db1' }, { name: 'foo' }]
+          },
+          {
+            type: 'table',
+            location: { first_line: 6, last_line: 6, first_column: 21, last_column: 24 },
+            identifierChain: [{ name: 'bar' }]
+          },
+          {
+            type: 'alias',
+            source: 'table',
+            alias: 'baa',
+            location: { first_line: 6, last_line: 6, first_column: 25, last_column: 28 },
+            identifierChain: [{ name: 'bar' }]
+          },
+          {
+            type: 'whereClause',
+            missing: false,
+            location: { first_line: 6, last_line: 6, first_column: 29, last_column: 67 }
+          },
+          {
+            type: 'table',
+            location: { first_line: 6, last_line: 6, first_column: 35, last_column: 38 },
+            identifierChain: [{ name: 'bar' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 6, last_line: 6, first_column: 39, last_column: 42 },
+            identifierChain: [{ name: 'bla' }],
+            tables: [{ identifierChain: [{ name: 'bar' }], alias: 'baa' }],
+            qualified: true
+          },
+          {
+            type: 'selectList',
+            missing: false,
+            location: { first_line: 6, last_line: 6, first_column: 54, last_column: 57 },
+            subquery: true
+          },
+          {
+            type: 'column',
+            location: { first_line: 6, last_line: 6, first_column: 54, last_column: 57 },
+            identifierChain: [{ name: 'ble' }],
+            tables: [{ identifierChain: [{ name: 'bla' }] }],
+            qualified: false
+          },
+          {
+            type: 'table',
+            location: { first_line: 6, last_line: 6, first_column: 63, last_column: 66 },
+            identifierChain: [{ name: 'bla' }]
+          },
+          {
+            type: 'whereClause',
+            subquery: true,
+            missing: true,
+            location: { first_line: 6, last_line: 6, first_column: 66, last_column: 66 }
+          },
+          {
+            type: 'limitClause',
+            subquery: true,
+            missing: true,
+            location: { first_line: 6, last_line: 6, first_column: 66, last_column: 66 }
+          },
+          {
+            type: 'limitClause',
+            missing: true,
+            location: { first_line: 6, last_line: 6, first_column: 67, last_column: 67 }
+          }
+        ]
+      });
+    }
+  );
+
+  it('should report locations for "SELECT tta.* FROM testTableA tta, testTableB; |"', () => {
+    assertLocations({
+      beforeCursor: 'SELECT tta.* FROM testTableA tta, testTableB; ',
+      expectedLocations: [
+        {
+          type: 'statement',
+          location: { first_line: 1, last_line: 1, first_column: 1, last_column: 45 }
+        },
+        {
+          type: 'selectList',
+          missing: false,
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 13 }
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 11 },
+          identifierChain: [{ name: 'testTableA' }]
+        },
+        {
+          type: 'asterisk',
+          location: { first_line: 1, last_line: 1, first_column: 12, last_column: 13 },
+          tables: [{ alias: 'tta', identifierChain: [{ name: 'testTableA' }] }]
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 19, last_column: 29 },
+          identifierChain: [{ name: 'testTableA' }]
+        },
+        {
+          type: 'alias',
+          source: 'table',
+          alias: 'tta',
+          location: { first_line: 1, last_line: 1, first_column: 30, last_column: 33 },
+          identifierChain: [{ name: 'testTableA' }]
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 35, last_column: 45 },
+          identifierChain: [{ name: 'testTableB' }]
+        },
+        {
+          type: 'whereClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 45, last_column: 45 }
+        },
+        {
+          type: 'limitClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 45, last_column: 45 }
+        }
+      ]
+    });
+  });
+
+  it('should report locations for "SELECT COUNT(*) FROM testTable; |"', () => {
+    assertLocations({
+      beforeCursor: 'SELECT COUNT(*) FROM testTable;',
+      expectedLocations: [
+        {
+          type: 'statement',
+          location: { first_line: 1, last_line: 1, first_column: 1, last_column: 31 }
+        },
+        {
+          type: 'selectList',
+          missing: false,
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 16 }
+        },
+        {
+          type: 'function',
+          location: { first_line: 1, last_line: 1, first_column: 8, last_column: 12 },
+          function: 'count'
+        },
+        {
+          type: 'functionArgument',
+          location: { first_line: 1, last_line: 1, first_column: 14, last_column: 15 },
+          function: 'count',
+          argumentPosition: 0,
+          identifierChain: [{ name: 'count' }],
+          expression: { text: '*' }
+        },
+        {
+          type: 'table',
+          location: { first_line: 1, last_line: 1, first_column: 22, last_column: 31 },
+          identifierChain: [{ name: 'testTable' }]
+        },
+        {
+          type: 'whereClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 31, last_column: 31 }
+        },
+        {
+          type: 'limitClause',
+          missing: true,
+          location: { first_line: 1, last_line: 1, first_column: 31, last_column: 31 }
+        }
+      ]
+    });
+  });
+});
+// prettier-ignore-end

File diff suppressed because it is too large
+ 91 - 0
desktop/core/src/desktop/js/parse/sql/sparksql/sparksqlAutocompleteParser.js


+ 35 - 0
desktop/core/src/desktop/js/parse/sql/sparksql/sparksqlAutocompleteParser.test.ts

@@ -0,0 +1,35 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// 'License'); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import sparksqlAutocompleteParser from './sparksqlAutocompleteParser';
+import structure from './jison/structure.json';
+import { AutocompleteParser } from '../../types';
+import { extractTestCases, runTestCases } from '../testUtils';
+import { assertPartials, CommonParser } from '../sharedParserTests';
+
+const jisonFolder = 'desktop/core/src/desktop/js/parse/sql/sparksql/jison';
+const groupedTestCases = extractTestCases(jisonFolder, structure.autocomplete);
+
+describe('sparksqlAutocompleteParser', () => {
+  // TODO: Fix the types
+  runTestCases(sparksqlAutocompleteParser as unknown as AutocompleteParser, groupedTestCases);
+
+  describe('partial removal', () => {
+    it('should identify part lengths', () => {
+      assertPartials(sparksqlAutocompleteParser as unknown as CommonParser);
+    });
+  });
+});

File diff suppressed because it is too large
+ 91 - 0
desktop/core/src/desktop/js/parse/sql/sparksql/sparksqlSyntaxParser.js


+ 179 - 0
desktop/core/src/desktop/js/parse/sql/sparksql/sparksqlSyntaxParser.test.js

@@ -0,0 +1,179 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// 'License'); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import sparksqlSyntaxParser from './sparksqlSyntaxParser';
+
+describe('sparksqlSyntaxParser.js', () => {
+  const expectedToStrings = function (expected) {
+    return expected.map(ex => ex.text);
+  };
+
+  it('should not find errors for ""', () => {
+    const result = sparksqlSyntaxParser.parseSyntax('', '');
+
+    expect(result).toBeFalsy();
+  });
+
+  it('should report incomplete statement for "SEL"', () => {
+    const result = sparksqlSyntaxParser.parseSyntax('SEL', '');
+
+    expect(result.incompleteStatement).toBeTruthy();
+  });
+
+  it('should report incomplete statement for "SELECT"', () => {
+    const result = sparksqlSyntaxParser.parseSyntax('SELECT', '');
+
+    expect(result.incompleteStatement).toBeTruthy();
+  });
+
+  it('should report incomplete statement for "SELECT "', () => {
+    const result = sparksqlSyntaxParser.parseSyntax('SELECT ', '');
+
+    expect(result.incompleteStatement).toBeTruthy();
+  });
+
+  it('should not report incomplete statement for "SELECT * FROM tbl"', () => {
+    const result = sparksqlSyntaxParser.parseSyntax('SELECT * FROM tbl', '');
+
+    expect(result.incompleteStatement).toBeFalsy();
+  });
+
+  it('should not report incomplete statement for "SELECT * FROM tbl LIMIT 1"', () => {
+    const result = sparksqlSyntaxParser.parseSyntax('SELECT * FROM tbl LIMIT 1', '');
+
+    expect(result.incompleteStatement).toBeFalsy();
+  });
+
+  it('should report incomplete statement for "SELECT * FROM tbl LIMIT "', () => {
+    const result = sparksqlSyntaxParser.parseSyntax('SELECT * FROM tbl LIMIT ', '');
+
+    expect(result.incompleteStatement).toBeTruthy();
+  });
+
+  it('should report incomplete statement for "SELECT * FROM tbl GROUP"', () => {
+    const result = sparksqlSyntaxParser.parseSyntax('SELECT * FROM tbl GROUP', '');
+
+    expect(result.incompleteStatement).toBeTruthy();
+  });
+
+  it('should not find errors for "SELECT *"', () => {
+    const result = sparksqlSyntaxParser.parseSyntax('SELECT *', '');
+
+    expect(result).toBeFalsy();
+  });
+
+  it('should not report incomplete statement for "SELECT * FR"', () => {
+    const result = sparksqlSyntaxParser.parseSyntax('SELECT * FR', '');
+
+    expect(result.incompleteStatement).toBeTruthy();
+  });
+
+  it('should find errors for "SLELECT "', () => {
+    const result = sparksqlSyntaxParser.parseSyntax('SLELECT ', '');
+
+    expect(result).toBeTruthy();
+    expect(result.text).toEqual('SLELECT');
+    expect(result.expected.length).toBeGreaterThan(0);
+    expect(result.loc.first_column).toEqual(0);
+    expect(result.loc.last_column).toEqual(7);
+  });
+
+  xit('should find errors for "alter tabel "', () => {
+    const result = sparksqlSyntaxParser.parseSyntax('alter tabel ', '');
+
+    expect(result).toBeTruthy();
+    expect(result.text).toEqual('tabel');
+    expect(result.expected.length).toBeGreaterThan(0);
+    expect(result.loc.first_column).toEqual(6);
+    expect(result.loc.last_column).toEqual(11);
+  });
+
+  it('should find errors for "select *  form "', () => {
+    const result = sparksqlSyntaxParser.parseSyntax('select *  form ', '');
+
+    expect(result).toBeTruthy();
+    expect(result.loc.first_column).toEqual(10);
+    expect(result.loc.last_column).toEqual(14);
+    expect(expectedToStrings(result.expected)).toEqual(['from', 'union']);
+  });
+
+  it('should find errors for "select * from customers c cultster by awasd asd afd;"', () => {
+    const result = sparksqlSyntaxParser.parseSyntax(
+      'select * from customers c cultster by awasd asd afd;',
+      ''
+    );
+
+    expect(result).toBeTruthy();
+  });
+
+  it('should find errors for "select asdf wer qwer qewr   qwer"', () => {
+    const result = sparksqlSyntaxParser.parseSyntax('select asdf wer qwer qewr   qwer', '');
+
+    expect(result).toBeTruthy();
+  });
+
+  it('should suggest expected words for "SLELECT "', () => {
+    const result = sparksqlSyntaxParser.parseSyntax('SLELECT ', '');
+
+    expect(result).toBeTruthy();
+    expect(expectedToStrings(result.expected).indexOf('SELECT')).not.toEqual(-1);
+  });
+
+  it('should suggest expected words for "slelect "', () => {
+    const result = sparksqlSyntaxParser.parseSyntax('slelect ', '');
+
+    expect(result).toBeTruthy();
+    expect(expectedToStrings(result.expected).indexOf('select')).not.toEqual(-1);
+  });
+
+  const expectEqualIds = function (beforeA, afterA, beforeB, afterB) {
+    const resultA = sparksqlSyntaxParser.parseSyntax(beforeA, afterA);
+    const resultB = sparksqlSyntaxParser.parseSyntax(beforeB, afterB);
+
+    expect(resultA).toBeTruthy();
+    expect(resultB).toBeTruthy();
+    expect(resultA.ruleId).toEqual(resultB.ruleId);
+  };
+
+  const expectNonEqualIds = function (beforeA, afterA, beforeB, afterB) {
+    const resultA = sparksqlSyntaxParser.parseSyntax(beforeA, afterA);
+    const resultB = sparksqlSyntaxParser.parseSyntax(beforeB, afterB);
+
+    expect(resultA).toBeTruthy();
+    expect(resultB).toBeTruthy();
+    expect(resultA.ruleId).not.toEqual(resultB.ruleId);
+  };
+
+  it('should have unique rule IDs when the same rule is failing in different locations', () => {
+    expectEqualIds('SLELECT ', '', 'dlrop ', '');
+    expectEqualIds('SELECT * FORM ', '', 'SELECT * bla ', '');
+    expectEqualIds('DROP TABLE b.bla ERRROROR ', '', 'DROP TABLE c.cla OTHERERRRRORRR ', '');
+    expectEqualIds(
+      'SELECT * FROM a WHERE id = 1, a b SELECT ',
+      '',
+      'SELECT id, foo FROM a WHERE a b SELECT',
+      ''
+    );
+    expectEqualIds(
+      'SELECT * FROM a WHERE id = 1, a b SELECT ',
+      '',
+      'SELECT id, foo FROM a WHERE a b SELECT',
+      ''
+    );
+
+    expectNonEqualIds('slelect ', '', 'select * form ', '');
+  });
+});

+ 1445 - 0
desktop/core/src/desktop/js/parse/sql/sparksql/sqlParseSupport.js

@@ -0,0 +1,1445 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { matchesType } from 'sql/reference/typeUtils';
+import {
+  initSharedAutocomplete,
+  initSyntaxParser,
+  identifierEquals,
+  equalIgnoreCase,
+  SIMPLE_TABLE_REF_SUGGESTIONS,
+  adjustForPartialBackticks
+} from 'parse/sql/sqlParseUtils';
+
+const initSqlParser = function (parser) {
+  initSharedAutocomplete(parser);
+
+  parser.prepareNewStatement = function () {
+    linkTablePrimaries();
+    parser.commitLocations();
+
+    delete parser.yy.latestCommonTableExpressions;
+    delete parser.yy.correlatedSubQuery;
+    parser.yy.subQueries = [];
+    parser.yy.selectListAliases = [];
+    parser.yy.latestTablePrimaries = [];
+
+    prioritizeSuggestions();
+  };
+
+  parser.yy.parseError = function (message, error) {
+    parser.yy.errors.push(error);
+    return message;
+  };
+
+  parser.addCommonTableExpressions = function (identifiers) {
+    parser.yy.result.commonTableExpressions = identifiers;
+    parser.yy.latestCommonTableExpressions = identifiers;
+  };
+
+  parser.isInSubquery = function () {
+    return !!parser.yy.primariesStack.length;
+  };
+
+  parser.pushQueryState = function () {
+    parser.yy.resultStack.push(parser.yy.result);
+    parser.yy.locationsStack.push(parser.yy.locations);
+    parser.yy.selectListAliasesStack.push(parser.yy.selectListAliases);
+    parser.yy.primariesStack.push(parser.yy.latestTablePrimaries);
+    parser.yy.subQueriesStack.push(parser.yy.subQueries);
+
+    parser.yy.result = {};
+    parser.yy.locations = [];
+    parser.yy.selectListAliases = []; // Not allowed in correlated sub-queries
+
+    if (parser.yy.correlatedSubQuery) {
+      parser.yy.latestTablePrimaries = parser.yy.latestTablePrimaries.concat();
+      parser.yy.subQueries = parser.yy.subQueries.concat();
+    } else {
+      parser.yy.latestTablePrimaries = [];
+      parser.yy.subQueries = [];
+    }
+  };
+
+  parser.popQueryState = function (subQuery) {
+    linkTablePrimaries();
+    parser.commitLocations();
+
+    if (Object.keys(parser.yy.result).length === 0) {
+      parser.yy.result = parser.yy.resultStack.pop();
+    } else {
+      parser.yy.resultStack.pop();
+    }
+    const oldSubQueries = parser.yy.subQueries;
+    parser.yy.subQueries = parser.yy.subQueriesStack.pop();
+    if (subQuery) {
+      if (oldSubQueries.length > 0) {
+        subQuery.subQueries = oldSubQueries;
+      }
+      parser.yy.subQueries.push(subQuery);
+    }
+
+    parser.yy.latestTablePrimaries = parser.yy.primariesStack.pop();
+    parser.yy.locations = parser.yy.locationsStack.pop();
+    parser.yy.selectListAliases = parser.yy.selectListAliasesStack.pop();
+  };
+
+  parser.suggestSelectListAliases = function () {
+    if (
+      parser.yy.selectListAliases &&
+      parser.yy.selectListAliases.length > 0 &&
+      parser.yy.result.suggestColumns &&
+      (typeof parser.yy.result.suggestColumns.identifierChain === 'undefined' ||
+        parser.yy.result.suggestColumns.identifierChain.length === 0)
+    ) {
+      parser.yy.result.suggestColumnAliases = parser.yy.selectListAliases;
+    }
+  };
+
+  parser.mergeSuggestKeywords = function () {
+    let result = [];
+    Array.prototype.slice.call(arguments).forEach(suggestion => {
+      if (typeof suggestion !== 'undefined' && typeof suggestion.suggestKeywords !== 'undefined') {
+        result = result.concat(suggestion.suggestKeywords);
+      }
+    });
+    if (result.length > 0) {
+      return { suggestKeywords: result };
+    }
+    return {};
+  };
+
+  parser.suggestValueExpressionKeywords = function (valueExpression, extras) {
+    const expressionKeywords = parser.getValueExpressionKeywords(valueExpression, extras);
+    parser.suggestKeywords(expressionKeywords.suggestKeywords);
+    if (expressionKeywords.suggestColRefKeywords) {
+      parser.suggestColRefKeywords(expressionKeywords.suggestColRefKeywords);
+    }
+    if (valueExpression.lastType) {
+      parser.addColRefIfExists(valueExpression.lastType);
+    } else {
+      parser.addColRefIfExists(valueExpression);
+    }
+  };
+
+  parser.getSelectListKeywords = function (excludeAsterisk) {
+    const keywords = [{ value: 'CASE', weight: 450 }, 'FALSE', 'TRUE', 'NULL'];
+    if (!excludeAsterisk) {
+      keywords.push({ value: '*', weight: 10000 });
+    }
+    return keywords;
+  };
+
+  parser.getValueExpressionKeywords = function (valueExpression, extras) {
+    const types = valueExpression.lastType ? valueExpression.lastType.types : valueExpression.types;
+    // We could have valueExpression.columnReference to suggest based on column type
+    let keywords = [
+      '<',
+      '<=',
+      '<=>',
+      '<>',
+      '=',
+      '>',
+      '>=',
+      'BETWEEN',
+      'IN',
+      'IS NOT NULL',
+      'IS NULL',
+      'IS NOT TRUE',
+      'IS TRUE',
+      'IS NOT FALSE',
+      'IS FALSE',
+      'NOT BETWEEN',
+      'NOT IN'
+    ];
+    if (extras) {
+      keywords = keywords.concat(extras);
+    }
+    if (valueExpression.suggestKeywords) {
+      keywords = keywords.concat(valueExpression.suggestKeywords);
+    }
+    if (types.length === 1 && types[0] === 'COLREF') {
+      return {
+        suggestKeywords: keywords,
+        suggestColRefKeywords: {
+          BOOLEAN: ['AND', 'OR'],
+          NUMBER: ['+', '-', '*', '/', '%', 'DIV'],
+          STRING: ['LIKE', 'NOT LIKE', 'REGEXP', 'RLIKE']
+        }
+      };
+    }
+    if (matchesType(parser.yy.activeDialect, ['BOOLEAN'], types)) {
+      keywords = keywords.concat(['AND', 'OR']);
+    }
+    if (matchesType(parser.yy.activeDialect, ['NUMBER'], types)) {
+      keywords = keywords.concat(['+', '-', '*', '/', '%', 'DIV']);
+    }
+    if (matchesType(parser.yy.activeDialect, ['STRING'], types)) {
+      keywords = keywords.concat(['LIKE', 'NOT LIKE', 'REGEXP', 'RLIKE']);
+    }
+    return { suggestKeywords: keywords };
+  };
+
+  parser.getTypeKeywords = function () {
+    return [
+      'BIGINT',
+      'BOOLEAN',
+      'CHAR',
+      'DECIMAL',
+      'DOUBLE',
+      'FLOAT',
+      'INT',
+      'SMALLINT',
+      'TIMESTAMP',
+      'STRING',
+      'TINYINT',
+      'VARCHAR'
+    ];
+  };
+
+  parser.getColumnDataTypeKeywords = function () {
+    return parser.getTypeKeywords();
+  };
+
+  parser.addColRefIfExists = function (valueExpression) {
+    if (valueExpression.columnReference) {
+      parser.yy.result.colRef = { identifierChain: valueExpression.columnReference };
+    }
+  };
+
+  parser.selectListNoTableSuggest = function (selectListEdit, hasDistinctOrAll) {
+    if (selectListEdit.cursorAtStart) {
+      let keywords = parser.getSelectListKeywords();
+      if (!hasDistinctOrAll) {
+        keywords = keywords.concat([
+          { value: 'ALL', weight: 2 },
+          { value: 'DISTINCT', weight: 2 }
+        ]);
+      }
+      parser.suggestKeywords(keywords);
+    } else {
+      parser.checkForKeywords(selectListEdit);
+    }
+    if (selectListEdit.suggestFunctions) {
+      parser.suggestFunctions();
+    }
+    if (selectListEdit.suggestColumns) {
+      parser.suggestColumns();
+    }
+    if (
+      selectListEdit.suggestAggregateFunctions &&
+      (!hasDistinctOrAll || hasDistinctOrAll === 'ALL')
+    ) {
+      parser.suggestAggregateFunctions();
+      parser.suggestAnalyticFunctions();
+    }
+  };
+
+  parser.suggestJoinConditions = function (details) {
+    parser.yy.result.suggestJoinConditions = details || {};
+    if (parser.yy.latestTablePrimaries && !parser.yy.result.suggestJoinConditions.tablePrimaries) {
+      parser.yy.result.suggestJoinConditions.tablePrimaries =
+        parser.yy.latestTablePrimaries.concat();
+    }
+  };
+
+  parser.suggestJoins = function (details) {
+    parser.yy.result.suggestJoins = details || {};
+  };
+
+  parser.findCaseType = function (whenThenList) {
+    const types = {};
+    whenThenList.caseTypes.forEach(valueExpression => {
+      valueExpression.types.forEach(type => {
+        types[type] = true;
+      });
+    });
+    if (Object.keys(types).length === 1) {
+      return { types: [Object.keys(types)[0]] };
+    }
+    return { types: ['T'] };
+  };
+
+  parser.applyArgumentTypesToSuggestions = function (functionName, position) {
+    if (parser.yy.result.suggestFunctions || parser.yy.result.suggestColumns) {
+      parser.yy.result.udfArgument = {
+        name: functionName.toLowerCase(),
+        position: position
+      };
+    }
+  };
+
+  parser.commitLocations = function () {
+    if (parser.yy.locations.length === 0) {
+      return;
+    }
+
+    const tablePrimaries = parser.yy.latestTablePrimaries;
+
+    let i = parser.yy.locations.length;
+
+    while (i--) {
+      const location = parser.yy.locations[i];
+      if (location.type === 'variable' && location.colRef) {
+        parser.expandIdentifierChain({
+          wrapper: location.colRef,
+          tablePrimaries: tablePrimaries,
+          isColumnWrapper: true
+        });
+        delete location.colRef.linked;
+      }
+
+      if (location.type === 'unknown') {
+        if (
+          typeof location.identifierChain !== 'undefined' &&
+          location.identifierChain.length > 0 &&
+          location.identifierChain.length <= 2 &&
+          tablePrimaries
+        ) {
+          let found = tablePrimaries.filter(primary => {
+            return (
+              equalIgnoreCase(primary.alias, location.identifierChain[0].name) ||
+              (primary.identifierChain &&
+                equalIgnoreCase(primary.identifierChain[0].name, location.identifierChain[0].name))
+            );
+          });
+          if (!found.length && location.firstInChain) {
+            found = tablePrimaries.filter(primary => {
+              return (
+                !primary.alias &&
+                primary.identifierChain &&
+                equalIgnoreCase(
+                  primary.identifierChain[primary.identifierChain.length - 1].name,
+                  location.identifierChain[0].name
+                )
+              );
+            });
+          }
+
+          if (found.length) {
+            if (
+              found[0].identifierChain.length > 1 &&
+              location.identifierChain.length === 1 &&
+              equalIgnoreCase(found[0].identifierChain[0].name, location.identifierChain[0].name)
+            ) {
+              location.type = 'database';
+            } else if (
+              found[0].alias &&
+              equalIgnoreCase(location.identifierChain[0].name, found[0].alias) &&
+              location.identifierChain.length > 1
+            ) {
+              location.type = 'column';
+              parser.expandIdentifierChain({
+                tablePrimaries: tablePrimaries,
+                wrapper: location,
+                anyOwner: true
+              });
+            } else if (
+              !found[0].alias &&
+              found[0].identifierChain &&
+              equalIgnoreCase(
+                location.identifierChain[0].name,
+                found[0].identifierChain[found[0].identifierChain.length - 1].name
+              ) &&
+              location.identifierChain.length > 1
+            ) {
+              location.type = 'column';
+              parser.expandIdentifierChain({
+                tablePrimaries: tablePrimaries,
+                wrapper: location,
+                anyOwner: true
+              });
+            } else {
+              location.type = 'table';
+              parser.expandIdentifierChain({
+                tablePrimaries: tablePrimaries,
+                wrapper: location,
+                anyOwner: true
+              });
+            }
+          } else if (parser.yy.subQueries) {
+            found = parser.yy.subQueries.filter(subQuery => {
+              return equalIgnoreCase(subQuery.alias, location.identifierChain[0].name);
+            });
+            if (found.length > 0) {
+              location.type = 'subQuery';
+              location.identifierChain = [{ subQuery: found[0].alias }];
+            }
+          }
+        }
+      }
+
+      if (location.type === 'asterisk' && !location.linked) {
+        if (tablePrimaries && tablePrimaries.length > 0) {
+          location.tables = [];
+          location.linked = false;
+          if (!location.identifierChain) {
+            location.identifierChain = [{ asterisk: true }];
+          }
+          parser.expandIdentifierChain({
+            tablePrimaries: tablePrimaries,
+            wrapper: location,
+            anyOwner: false
+          });
+          if (location.tables.length === 0) {
+            parser.yy.locations.splice(i, 1);
+          }
+        } else {
+          parser.yy.locations.splice(i, 1);
+        }
+      }
+
+      if (
+        location.type === 'table' &&
+        typeof location.identifierChain !== 'undefined' &&
+        location.identifierChain.length === 1 &&
+        location.identifierChain[0].name
+      ) {
+        // Could be a cte reference
+        parser.yy.locations.some(otherLocation => {
+          if (
+            otherLocation.type === 'alias' &&
+            otherLocation.source === 'cte' &&
+            identifierEquals(otherLocation.alias, location.identifierChain[0].name)
+          ) {
+            // TODO: Possibly add the other location if we want to show the link in the future.
+            //       i.e. highlight select definition on hover over alias, also for subquery references.
+            location.type = 'alias';
+            location.target = 'cte';
+            location.alias = location.identifierChain[0].name;
+            delete location.identifierChain;
+            return true;
+          }
+        });
+      }
+
+      if (
+        location.type === 'table' &&
+        (typeof location.identifierChain === 'undefined' || location.identifierChain.length === 0)
+      ) {
+        parser.yy.locations.splice(i, 1);
+      }
+
+      if (location.type === 'unknown') {
+        location.type = 'column';
+      }
+
+      // A column location might refer to a previously defined alias, i.e. last 'foo' in "SELECT cast(id AS int) foo FROM tbl ORDER BY foo;"
+      if (location.type === 'column') {
+        for (let j = i - 1; j >= 0; j--) {
+          const otherLocation = parser.yy.locations[j];
+          if (
+            otherLocation.type === 'alias' &&
+            otherLocation.source === 'column' &&
+            location.identifierChain &&
+            location.identifierChain.length === 1 &&
+            location.identifierChain[0].name &&
+            otherLocation.alias &&
+            location.identifierChain[0].name.toLowerCase() === otherLocation.alias.toLowerCase()
+          ) {
+            location.type = 'alias';
+            location.source = 'column';
+            location.alias = location.identifierChain[0].name;
+            delete location.identifierChain;
+            location.parentLocation = otherLocation.parentLocation;
+            break;
+          }
+        }
+      }
+
+      if (location.type === 'column') {
+        const initialIdentifierChain = location.identifierChain
+          ? location.identifierChain.concat()
+          : undefined;
+
+        parser.expandIdentifierChain({
+          tablePrimaries: tablePrimaries,
+          wrapper: location,
+          anyOwner: true,
+          isColumnWrapper: true,
+          isColumnLocation: true
+        });
+
+        if (typeof location.identifierChain === 'undefined') {
+          parser.yy.locations.splice(i, 1);
+        } else if (
+          location.identifierChain.length === 0 &&
+          initialIdentifierChain &&
+          initialIdentifierChain.length === 1
+        ) {
+          // This is for the case "SELECT tblOrColName FROM db.tblOrColName";
+          location.identifierChain = initialIdentifierChain;
+        }
+      }
+      if (location.type === 'column' && location.identifierChain) {
+        if (location.identifierChain.length > 1 && location.tables && location.tables.length > 0) {
+          location.type = 'complex';
+        }
+      }
+      delete location.firstInChain;
+      if (location.type !== 'column' && location.type !== 'complex') {
+        delete location.qualified;
+      } else if (typeof location.qualified === 'undefined') {
+        location.qualified = false;
+      }
+    }
+
+    if (parser.yy.locations.length > 0) {
+      parser.yy.allLocations = parser.yy.allLocations.concat(parser.yy.locations);
+      parser.yy.locations = [];
+    }
+  };
+
+  const prioritizeSuggestions = function () {
+    parser.yy.result.lowerCase = parser.yy.lowerCase || false;
+
+    const cteIndex = {};
+
+    if (typeof parser.yy.latestCommonTableExpressions !== 'undefined') {
+      parser.yy.latestCommonTableExpressions.forEach(cte => {
+        cteIndex[cte.alias.toLowerCase()] = cte;
+      });
+    }
+
+    SIMPLE_TABLE_REF_SUGGESTIONS.forEach(suggestionType => {
+      if (
+        suggestionType !== 'suggestAggregateFunctions' &&
+        typeof parser.yy.result[suggestionType] !== 'undefined' &&
+        parser.yy.result[suggestionType].tables.length === 0
+      ) {
+        delete parser.yy.result[suggestionType];
+      } else if (
+        typeof parser.yy.result[suggestionType] !== 'undefined' &&
+        typeof parser.yy.result[suggestionType].tables !== 'undefined'
+      ) {
+        for (let i = parser.yy.result[suggestionType].tables.length - 1; i >= 0; i--) {
+          const table = parser.yy.result[suggestionType].tables[i];
+          if (
+            table.identifierChain.length === 1 &&
+            typeof table.identifierChain[0].name !== 'undefined' &&
+            typeof cteIndex[table.identifierChain[0].name.toLowerCase()] !== 'undefined'
+          ) {
+            parser.yy.result[suggestionType].tables.splice(i, 1);
+          }
+        }
+      }
+    });
+
+    if (typeof parser.yy.result.colRef !== 'undefined') {
+      if (
+        !parser.yy.result.colRef.linked ||
+        typeof parser.yy.result.colRef.identifierChain === 'undefined' ||
+        parser.yy.result.colRef.identifierChain.length === 0
+      ) {
+        delete parser.yy.result.colRef;
+        if (typeof parser.yy.result.suggestColRefKeywords !== 'undefined') {
+          Object.keys(parser.yy.result.suggestColRefKeywords).forEach(type => {
+            parser.yy.result.suggestKeywords = parser.yy.result.suggestKeywords.concat(
+              parser.createWeightedKeywords(parser.yy.result.suggestColRefKeywords[type], -1)
+            );
+          });
+          delete parser.yy.result.suggestColRefKeywords;
+        }
+        if (
+          parser.yy.result.suggestColumns &&
+          parser.yy.result.suggestColumns.types.length === 1 &&
+          parser.yy.result.suggestColumns.types[0] === 'COLREF'
+        ) {
+          parser.yy.result.suggestColumns.types = ['T'];
+        }
+        delete parser.yy.result.suggestValues;
+      }
+    }
+
+    if (typeof parser.yy.result.colRef !== 'undefined') {
+      if (
+        !parser.yy.result.suggestValues &&
+        !parser.yy.result.suggestColRefKeywords &&
+        (!parser.yy.result.suggestColumns || parser.yy.result.suggestColumns.types[0] !== 'COLREF')
+      ) {
+        delete parser.yy.result.colRef;
+      }
+    }
+    if (
+      typeof parser.yy.result.suggestIdentifiers !== 'undefined' &&
+      parser.yy.result.suggestIdentifiers.length > 0
+    ) {
+      delete parser.yy.result.suggestTables;
+      delete parser.yy.result.suggestDatabases;
+    }
+    if (typeof parser.yy.result.suggestColumns !== 'undefined') {
+      const suggestColumns = parser.yy.result.suggestColumns;
+      if (typeof suggestColumns.tables === 'undefined' || suggestColumns.tables.length === 0) {
+        delete parser.yy.result.suggestColumns;
+        delete parser.yy.result.subQueries;
+      } else {
+        delete parser.yy.result.suggestTables;
+        delete parser.yy.result.suggestDatabases;
+
+        suggestColumns.tables.forEach(table => {
+          if (
+            typeof table.identifierChain !== 'undefined' &&
+            table.identifierChain.length === 1 &&
+            typeof table.identifierChain[0].name !== 'undefined'
+          ) {
+            const cte = cteIndex[table.identifierChain[0].name.toLowerCase()];
+            if (typeof cte !== 'undefined') {
+              delete table.identifierChain[0].name;
+              table.identifierChain[0].cte = cte.alias;
+            }
+          } else if (typeof table.identifierChain === 'undefined' && table.subQuery) {
+            table.identifierChain = [{ subQuery: table.subQuery }];
+            delete table.subQuery;
+          }
+        });
+
+        if (
+          typeof suggestColumns.identifierChain !== 'undefined' &&
+          suggestColumns.identifierChain.length === 0
+        ) {
+          delete suggestColumns.identifierChain;
+        }
+      }
+    } else {
+      delete parser.yy.result.subQueries;
+    }
+
+    if (typeof parser.yy.result.suggestJoinConditions !== 'undefined') {
+      if (
+        typeof parser.yy.result.suggestJoinConditions.tables === 'undefined' ||
+        parser.yy.result.suggestJoinConditions.tables.length === 0
+      ) {
+        delete parser.yy.result.suggestJoinConditions;
+      }
+    }
+
+    if (
+      typeof parser.yy.result.suggestTables !== 'undefined' &&
+      typeof parser.yy.result.commonTableExpressions !== 'undefined'
+    ) {
+      const ctes = [];
+      parser.yy.result.commonTableExpressions.forEach(cte => {
+        const suggestion = { name: cte.alias };
+        if (parser.yy.result.suggestTables.prependFrom) {
+          suggestion.prependFrom = true;
+        }
+        if (parser.yy.result.suggestTables.prependQuestionMark) {
+          suggestion.prependQuestionMark = true;
+        }
+        ctes.push(suggestion);
+      });
+      if (ctes.length > 0) {
+        parser.yy.result.suggestCommonTableExpressions = ctes;
+      }
+    }
+  };
+
+  const addCleanTablePrimary = function (tables, tablePrimary) {
+    if (tablePrimary.alias) {
+      tables.push({ alias: tablePrimary.alias, identifierChain: tablePrimary.identifierChain });
+    } else {
+      tables.push({ identifierChain: tablePrimary.identifierChain });
+    }
+  };
+
+  parser.expandIdentifierChain = function (options) {
+    const wrapper = options.wrapper;
+    const anyOwner = options.anyOwner;
+    const isColumnWrapper = options.isColumnWrapper;
+    const isColumnLocation = options.isColumnLocation;
+    let tablePrimaries = options.tablePrimaries || parser.yy.latestTablePrimaries;
+
+    if (typeof wrapper.identifierChain === 'undefined' || typeof tablePrimaries === 'undefined') {
+      return;
+    }
+    let identifierChain = wrapper.identifierChain.concat();
+
+    if (tablePrimaries.length === 0) {
+      delete wrapper.identifierChain;
+      return;
+    }
+
+    if (!anyOwner) {
+      tablePrimaries = filterTablePrimariesForOwner(tablePrimaries, wrapper.owner);
+    }
+
+    if (identifierChain.length > 0 && identifierChain[identifierChain.length - 1].asterisk) {
+      const tables = [];
+      tablePrimaries.forEach(tablePrimary => {
+        if (identifierChain.length > 1 && !tablePrimary.subQueryAlias) {
+          if (
+            identifierChain.length === 2 &&
+            equalIgnoreCase(tablePrimary.alias, identifierChain[0].name)
+          ) {
+            addCleanTablePrimary(tables, tablePrimary);
+          } else if (
+            identifierChain.length === 2 &&
+            equalIgnoreCase(tablePrimary.identifierChain[0].name, identifierChain[0].name)
+          ) {
+            addCleanTablePrimary(tables, tablePrimary);
+          } else if (
+            identifierChain.length === 3 &&
+            tablePrimary.identifierChain.length > 1 &&
+            equalIgnoreCase(tablePrimary.identifierChain[0].name, identifierChain[0].name) &&
+            equalIgnoreCase(tablePrimary.identifierChain[1].name, identifierChain[1].name)
+          ) {
+            addCleanTablePrimary(tables, tablePrimary);
+          }
+        } else if (tablePrimary.subQueryAlias) {
+          tables.push({ identifierChain: [{ subQuery: tablePrimary.subQueryAlias }] });
+        } else {
+          addCleanTablePrimary(tables, tablePrimary);
+        }
+      });
+      // Possible Joins
+      if (tables.length > 0) {
+        wrapper.tables = tables;
+        delete wrapper.identifierChain;
+        return;
+      }
+    }
+
+    // IdentifierChain contains a possibly started identifier or empty, example: a.b.c = ['a', 'b', 'c']
+    // Reduce the tablePrimaries to the one that matches the first identifier if found
+    let foundPrimary;
+    let doubleMatch = false;
+    let aliasMatch = false;
+    if (identifierChain.length > 0) {
+      for (let i = 0; i < tablePrimaries.length; i++) {
+        if (tablePrimaries[i].subQueryAlias) {
+          if (equalIgnoreCase(tablePrimaries[i].subQueryAlias, identifierChain[0].name)) {
+            foundPrimary = tablePrimaries[i];
+          }
+        } else if (equalIgnoreCase(tablePrimaries[i].alias, identifierChain[0].name)) {
+          foundPrimary = tablePrimaries[i];
+          aliasMatch = true;
+          break;
+        } else if (
+          tablePrimaries[i].identifierChain.length > 1 &&
+          identifierChain.length > 1 &&
+          equalIgnoreCase(tablePrimaries[i].identifierChain[0].name, identifierChain[0].name) &&
+          equalIgnoreCase(tablePrimaries[i].identifierChain[1].name, identifierChain[1].name)
+        ) {
+          foundPrimary = tablePrimaries[i];
+          doubleMatch = true;
+          break;
+        } else if (
+          !foundPrimary &&
+          equalIgnoreCase(tablePrimaries[i].identifierChain[0].name, identifierChain[0].name) &&
+          identifierChain.length > (isColumnLocation ? 1 : 0)
+        ) {
+          foundPrimary = tablePrimaries[i];
+          // No break as first two can still match.
+        } else if (
+          !foundPrimary &&
+          tablePrimaries[i].identifierChain.length > 1 &&
+          !tablePrimaries[i].alias &&
+          equalIgnoreCase(
+            tablePrimaries[i].identifierChain[tablePrimaries[i].identifierChain.length - 1].name,
+            identifierChain[0].name
+          )
+        ) {
+          // This is for the case SELECT baa. FROM bla.baa, blo.boo;
+          foundPrimary = tablePrimaries[i];
+          break;
+        }
+      }
+    }
+
+    if (foundPrimary) {
+      identifierChain.shift();
+      if (doubleMatch) {
+        identifierChain.shift();
+      }
+    } else if (tablePrimaries.length === 1 && !isColumnWrapper) {
+      foundPrimary = tablePrimaries[0];
+    }
+
+    if (foundPrimary) {
+      if (isColumnWrapper) {
+        wrapper.identifierChain = identifierChain;
+        if (foundPrimary.subQueryAlias) {
+          wrapper.tables = [{ subQuery: foundPrimary.subQueryAlias }];
+        } else if (foundPrimary.alias) {
+          if (!isColumnLocation && isColumnWrapper && aliasMatch) {
+            // TODO: add alias on table in suggestColumns (needs support in sqlAutocomplete3.js)
+            // the case is: SELECT cu.| FROM customers cu;
+            // This prevents alias from being added automatically in sqlAutocompleter.js
+            wrapper.tables = [{ identifierChain: foundPrimary.identifierChain }];
+          } else {
+            wrapper.tables = [
+              { identifierChain: foundPrimary.identifierChain, alias: foundPrimary.alias }
+            ];
+          }
+        } else {
+          wrapper.tables = [{ identifierChain: foundPrimary.identifierChain }];
+        }
+      } else {
+        if (foundPrimary.subQueryAlias) {
+          identifierChain.unshift({ subQuery: foundPrimary.subQueryAlias });
+        } else {
+          identifierChain = foundPrimary.identifierChain.concat(identifierChain);
+        }
+        if (wrapper.tables) {
+          wrapper.tables.push({ identifierChain: identifierChain });
+          delete wrapper.identifierChain;
+        } else {
+          wrapper.identifierChain = identifierChain;
+        }
+      }
+    } else {
+      if (isColumnWrapper) {
+        wrapper.tables = [];
+      }
+      tablePrimaries.forEach(tablePrimary => {
+        const targetTable = tablePrimary.subQueryAlias
+          ? { subQuery: tablePrimary.subQueryAlias }
+          : { identifierChain: tablePrimary.identifierChain };
+        if (tablePrimary.alias) {
+          targetTable.alias = tablePrimary.alias;
+        }
+        if (wrapper.tables) {
+          wrapper.tables.push(targetTable);
+        }
+      });
+    }
+    delete wrapper.owner;
+    wrapper.linked = true;
+  };
+
+  const filterTablePrimariesForOwner = function (tablePrimaries, owner) {
+    const result = [];
+    tablePrimaries.forEach(primary => {
+      if (typeof owner === 'undefined' && typeof primary.owner === 'undefined') {
+        result.push(primary);
+      } else if (owner === primary.owner) {
+        result.push(primary);
+      }
+    });
+    return result;
+  };
+
+  const convertTablePrimariesToSuggestions = function (tablePrimaries) {
+    const tables = [];
+    const identifiers = [];
+    tablePrimaries.forEach(tablePrimary => {
+      if (tablePrimary.identifierChain && tablePrimary.identifierChain.length > 0) {
+        const table = { identifierChain: tablePrimary.identifierChain };
+        if (tablePrimary.alias) {
+          table.alias = tablePrimary.alias;
+          identifiers.push({ name: table.alias + '.', type: 'alias' });
+        } else {
+          const lastIdentifier =
+            tablePrimary.identifierChain[tablePrimary.identifierChain.length - 1];
+          if (typeof lastIdentifier.name !== 'undefined') {
+            identifiers.push({ name: lastIdentifier.name + '.', type: 'table' });
+          } else if (typeof lastIdentifier.subQuery !== 'undefined') {
+            identifiers.push({ name: lastIdentifier.subQuery + '.', type: 'sub-query' });
+          }
+        }
+        tables.push(table);
+      } else if (tablePrimary.subQueryAlias) {
+        identifiers.push({ name: tablePrimary.subQueryAlias + '.', type: 'sub-query' });
+        tables.push({ identifierChain: [{ subQuery: tablePrimary.subQueryAlias }] });
+      }
+    });
+    if (identifiers.length > 0) {
+      if (typeof parser.yy.result.suggestIdentifiers === 'undefined') {
+        parser.yy.result.suggestIdentifiers = identifiers;
+      } else {
+        parser.yy.result.suggestIdentifiers = identifiers.concat(
+          parser.yy.result.suggestIdentifiers
+        );
+      }
+    }
+    parser.yy.result.suggestColumns.tables = tables;
+    if (
+      parser.yy.result.suggestColumns.identifierChain &&
+      parser.yy.result.suggestColumns.identifierChain.length === 0
+    ) {
+      delete parser.yy.result.suggestColumns.identifierChain;
+    }
+    parser.yy.result.suggestColumns.linked = true;
+  };
+
+  const linkTablePrimaries = function () {
+    if (!parser.yy.cursorFound || typeof parser.yy.latestTablePrimaries === 'undefined') {
+      return;
+    }
+
+    SIMPLE_TABLE_REF_SUGGESTIONS.forEach(suggestionType => {
+      if (
+        typeof parser.yy.result[suggestionType] !== 'undefined' &&
+        parser.yy.result[suggestionType].tablePrimaries &&
+        !parser.yy.result[suggestionType].linked
+      ) {
+        parser.yy.result[suggestionType].tables = [];
+        parser.yy.result[suggestionType].tablePrimaries.forEach(tablePrimary => {
+          if (!tablePrimary.subQueryAlias) {
+            parser.yy.result[suggestionType].tables.push(
+              tablePrimary.alias
+                ? {
+                    identifierChain: tablePrimary.identifierChain.concat(),
+                    alias: tablePrimary.alias
+                  }
+                : { identifierChain: tablePrimary.identifierChain.concat() }
+            );
+          }
+        });
+        delete parser.yy.result[suggestionType].tablePrimaries;
+        parser.yy.result[suggestionType].linked = true;
+      }
+    });
+
+    if (
+      typeof parser.yy.result.suggestColumns !== 'undefined' &&
+      !parser.yy.result.suggestColumns.linked
+    ) {
+      const tablePrimaries = filterTablePrimariesForOwner(
+        parser.yy.latestTablePrimaries,
+        parser.yy.result.suggestColumns.owner
+      );
+      if (!parser.yy.result.suggestColumns.tables) {
+        parser.yy.result.suggestColumns.tables = [];
+      }
+      if (parser.yy.subQueries.length > 0) {
+        parser.yy.result.subQueries = parser.yy.subQueries;
+      }
+      if (
+        typeof parser.yy.result.suggestColumns.identifierChain === 'undefined' ||
+        parser.yy.result.suggestColumns.identifierChain.length === 0
+      ) {
+        if (tablePrimaries.length > 1) {
+          convertTablePrimariesToSuggestions(tablePrimaries);
+        } else {
+          if (
+            tablePrimaries.length === 1 &&
+            (tablePrimaries[0].alias || tablePrimaries[0].subQueryAlias)
+          ) {
+            convertTablePrimariesToSuggestions(tablePrimaries);
+          }
+          parser.expandIdentifierChain({
+            wrapper: parser.yy.result.suggestColumns,
+            anyOwner: false,
+            isColumnWrapper: true
+          });
+        }
+      } else {
+        parser.expandIdentifierChain({
+          wrapper: parser.yy.result.suggestColumns,
+          anyOwner: false,
+          isColumnWrapper: true
+        });
+      }
+    }
+
+    if (typeof parser.yy.result.colRef !== 'undefined' && !parser.yy.result.colRef.linked) {
+      parser.expandIdentifierChain({ wrapper: parser.yy.result.colRef });
+
+      const primaries = filterTablePrimariesForOwner(parser.yy.latestTablePrimaries);
+      if (
+        primaries.length === 0 ||
+        (primaries.length > 1 && parser.yy.result.colRef.identifierChain.length === 1)
+      ) {
+        parser.yy.result.colRef.identifierChain = [];
+      }
+    }
+    if (
+      typeof parser.yy.result.suggestKeyValues !== 'undefined' &&
+      !parser.yy.result.suggestKeyValues.linked
+    ) {
+      parser.expandIdentifierChain({ wrapper: parser.yy.result.suggestKeyValues });
+    }
+  };
+
+  parser.addTablePrimary = function (ref) {
+    if (typeof parser.yy.latestTablePrimaries === 'undefined') {
+      parser.yy.latestTablePrimaries = [];
+    }
+    parser.yy.latestTablePrimaries.push(ref);
+  };
+
+  parser.suggestFileFormats = function () {
+    parser.suggestKeywords([
+      'AVRO',
+      'KUDU',
+      'ORC',
+      'PARQUET',
+      'RCFILE',
+      'SEQUENCEFILE',
+      'TEXTFILE'
+    ]);
+  };
+
+  parser.getKeywordsForOptionalsLR = function (optionals, keywords, override) {
+    let result = [];
+
+    for (let i = 0; i < optionals.length; i++) {
+      if (!optionals[i] && (typeof override === 'undefined' || override[i])) {
+        if (keywords[i] instanceof Array) {
+          result = result.concat(keywords[i]);
+        } else {
+          result.push(keywords[i]);
+        }
+      } else if (optionals[i]) {
+        break;
+      }
+    }
+    return result;
+  };
+
+  parser.suggestDdlAndDmlKeywords = function (extraKeywords) {
+    let keywords = ['CREATE', 'DESCRIBE', 'SELECT', 'SHOW'];
+
+    if (extraKeywords) {
+      keywords = keywords.concat(extraKeywords);
+    }
+
+    parser.suggestKeywords(keywords);
+  };
+
+  parser.checkForSelectListKeywords = function (selectList) {
+    if (selectList.length === 0) {
+      return;
+    }
+    const last = selectList[selectList.length - 1];
+    if (!last || !last.valueExpression) {
+      return;
+    }
+    const valueExpressionKeywords = parser.getValueExpressionKeywords(last.valueExpression);
+    let keywords = [];
+    if (last.suggestKeywords) {
+      keywords = keywords.concat(last.suggestKeywords);
+    }
+    if (valueExpressionKeywords.suggestKeywords) {
+      keywords = keywords.concat(valueExpressionKeywords.suggestKeywords);
+    }
+    if (valueExpressionKeywords.suggestColRefKeywords) {
+      parser.suggestColRefKeywords(valueExpressionKeywords.suggestColRefKeywords);
+      parser.addColRefIfExists(last.valueExpression);
+    }
+    if (!last.alias) {
+      keywords.push('AS');
+    }
+    if (keywords.length > 0) {
+      parser.suggestKeywords(keywords);
+    }
+  };
+
+  parser.checkForKeywords = function (expression) {
+    if (expression) {
+      if (expression.suggestKeywords && expression.suggestKeywords.length > 0) {
+        parser.suggestKeywords(expression.suggestKeywords);
+      }
+      if (expression.suggestColRefKeywords) {
+        parser.suggestColRefKeywords(expression.suggestColRefKeywords);
+        parser.addColRefIfExists(expression);
+      }
+    }
+  };
+
+  parser.createWeightedKeywords = function (keywords, weight) {
+    const result = [];
+    keywords.forEach(keyword => {
+      if (typeof keyword.weight !== 'undefined') {
+        keyword.weight = weight + keyword.weight / 10;
+        result.push(keyword);
+      } else {
+        result.push({ value: keyword, weight: weight });
+      }
+    });
+    return result;
+  };
+
+  parser.suggestKeywords = function (keywords) {
+    const weightedKeywords = [];
+    if (keywords.length === 0) {
+      return;
+    }
+    keywords.forEach(keyword => {
+      if (typeof keyword.weight !== 'undefined') {
+        weightedKeywords.push(keyword);
+      } else {
+        weightedKeywords.push({ value: keyword, weight: -1 });
+      }
+    });
+    weightedKeywords.sort((a, b) => {
+      if (a.weight !== b.weight) {
+        return b.weight - a.weight;
+      }
+      return a.value.localeCompare(b.value);
+    });
+    parser.yy.result.suggestKeywords = weightedKeywords;
+  };
+
+  parser.suggestColRefKeywords = function (colRefKeywords) {
+    parser.yy.result.suggestColRefKeywords = colRefKeywords;
+  };
+
+  parser.suggestTablesOrColumns = function (identifier) {
+    if (typeof parser.yy.latestTablePrimaries == 'undefined') {
+      parser.suggestTables({ identifierChain: [{ name: identifier }] });
+      return;
+    }
+    const tableRef = parser.yy.latestTablePrimaries.filter(tablePrimary => {
+      return equalIgnoreCase(tablePrimary.alias, identifier);
+    });
+    if (tableRef.length > 0) {
+      parser.suggestColumns({ identifierChain: [{ name: identifier }] });
+    } else {
+      parser.suggestTables({ identifierChain: [{ name: identifier }] });
+    }
+  };
+
+  parser.suggestFunctions = function (details) {
+    parser.yy.result.suggestFunctions = details || {};
+  };
+
+  parser.suggestAggregateFunctions = function () {
+    const primaries = [];
+    const aliases = {};
+    parser.yy.latestTablePrimaries.forEach(primary => {
+      if (typeof primary.alias !== 'undefined') {
+        aliases[primary.alias] = true;
+      }
+      // Drop if the first one refers to a table alias (...FROM tbl t, t.map tm ...)
+      if (
+        typeof primary.identifierChain !== 'undefined' &&
+        !aliases[primary.identifierChain[0].name] &&
+        typeof primary.owner === 'undefined'
+      ) {
+        primaries.push(primary);
+      }
+    });
+    parser.yy.result.suggestAggregateFunctions = { tablePrimaries: primaries };
+  };
+
+  parser.suggestAnalyticFunctions = function () {
+    parser.yy.result.suggestAnalyticFunctions = true;
+  };
+
+  parser.suggestSetOptions = function () {
+    parser.yy.result.suggestSetOptions = true;
+  };
+
+  parser.suggestIdentifiers = function (identifiers) {
+    parser.yy.result.suggestIdentifiers = identifiers;
+  };
+
+  parser.suggestColumns = function (details) {
+    if (typeof details === 'undefined') {
+      details = { identifierChain: [] };
+    } else if (typeof details.identifierChain === 'undefined') {
+      details.identifierChain = [];
+    }
+    parser.yy.result.suggestColumns = details;
+  };
+
+  parser.suggestGroupBys = function (details) {
+    parser.yy.result.suggestGroupBys = details || {};
+  };
+
+  parser.suggestOrderBys = function (details) {
+    parser.yy.result.suggestOrderBys = details || {};
+  };
+
+  parser.suggestFilters = function (details) {
+    parser.yy.result.suggestFilters = details || {};
+  };
+
+  parser.suggestKeyValues = function (details) {
+    parser.yy.result.suggestKeyValues = details || {};
+  };
+
+  parser.suggestTables = function (details) {
+    parser.yy.result.suggestTables = details || {};
+  };
+
+  parser.firstDefined = function () {
+    for (let i = 0; i + 1 < arguments.length; i += 2) {
+      if (arguments[i]) {
+        return arguments[i + 1];
+      }
+    }
+  };
+
+  parser.addColRefToVariableIfExists = function (left, right) {
+    if (
+      left &&
+      left.columnReference &&
+      left.columnReference.length &&
+      right &&
+      right.columnReference &&
+      right.columnReference.length &&
+      parser.yy.locations.length > 1
+    ) {
+      const addColRefToVariableLocation = function (variableValue, colRef) {
+        // See if colref is actually an alias
+        if (colRef.length === 1 && colRef[0].name) {
+          parser.yy.locations.some(location => {
+            if (location.type === 'column' && location.alias === colRef[0].name) {
+              colRef = location.identifierChain;
+              return true;
+            }
+          });
+        }
+
+        for (let i = parser.yy.locations.length - 1; i > 0; i--) {
+          const location = parser.yy.locations[i];
+          if (location.type === 'variable' && location.value === variableValue) {
+            location.colRef = { identifierChain: colRef };
+            break;
+          }
+        }
+      };
+
+      if (/\${[^}]*}/.test(left.columnReference[0].name)) {
+        // left is variable
+        addColRefToVariableLocation(left.columnReference[0].name, right.columnReference);
+      } else if (/\${[^}]*}/.test(right.columnReference[0].name)) {
+        // right is variable
+        addColRefToVariableLocation(right.columnReference[0].name, left.columnReference);
+      }
+    }
+  };
+
+  parser.suggestDatabases = function (details) {
+    parser.yy.result.suggestDatabases = details || {};
+  };
+
+  parser.suggestHdfs = function (details) {
+    parser.yy.result.suggestHdfs = details || {};
+  };
+
+  parser.suggestValues = function (details) {
+    parser.yy.result.suggestValues = details || {};
+  };
+
+  parser.determineCase = function (text) {
+    if (!parser.yy.caseDetermined) {
+      parser.yy.lowerCase = text.toLowerCase() === text;
+      parser.yy.caseDetermined = true;
+    }
+  };
+
+  parser.handleQuotedValueWithCursor = function (lexer, yytext, yylloc, quoteChar) {
+    if (yytext.indexOf('\u2020') !== -1 || yytext.indexOf('\u2021') !== -1) {
+      parser.yy.partialCursor = yytext.indexOf('\u2021') !== -1;
+      const cursorIndex = parser.yy.partialCursor
+        ? yytext.indexOf('\u2021')
+        : yytext.indexOf('\u2020');
+      parser.yy.cursorFound = {
+        first_line: yylloc.first_line,
+        last_line: yylloc.last_line,
+        first_column: yylloc.first_column + cursorIndex,
+        last_column: yylloc.first_column + cursorIndex + 1
+      };
+      const remainder = yytext.substring(cursorIndex + 1);
+      const remainingQuotes = (lexer.upcomingInput().match(new RegExp(quoteChar, 'g')) || [])
+        .length;
+      if (remainingQuotes > 0 && (remainingQuotes & 1) !== 0) {
+        parser.yy.missingEndQuote = false;
+        lexer.input();
+      } else {
+        parser.yy.missingEndQuote = true;
+        lexer.unput(remainder);
+      }
+      lexer.popState();
+      return true;
+    }
+    return false;
+  };
+
+  let lexerModified = false;
+
+  /**
+   * Main parser function
+   */
+  parser.parseSql = function (beforeCursor, afterCursor, debug) {
+    // Jison counts CRLF as two lines in the locations
+    beforeCursor = beforeCursor.replace(/\r\n|\n\r/gm, '\n');
+    afterCursor = afterCursor.replace(/\r\n|\n\r/gm, '\n');
+    parser.yy.result = { locations: [] };
+    parser.yy.lowerCase = false;
+    parser.yy.locations = [];
+    parser.yy.definitions = [];
+    parser.yy.allLocations = [];
+    parser.yy.subQueries = [];
+    parser.yy.errors = [];
+    parser.yy.selectListAliases = [];
+    parser.yy.activeDialect = 'generic';
+
+    parser.yy.locationsStack = [];
+    parser.yy.primariesStack = [];
+    parser.yy.subQueriesStack = [];
+    parser.yy.resultStack = [];
+    parser.yy.selectListAliasesStack = [];
+
+    delete parser.yy.caseDetermined;
+    delete parser.yy.cursorFound;
+    delete parser.yy.partialCursor;
+
+    // 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;
+    }
+
+    parser.prepareNewStatement();
+
+    const REASONABLE_SURROUNDING_LENGTH = 150000; // About 3000 lines before and after
+
+    if (beforeCursor.length > REASONABLE_SURROUNDING_LENGTH) {
+      if (beforeCursor.length - beforeCursor.lastIndexOf(';') > REASONABLE_SURROUNDING_LENGTH) {
+        // Bail out if the last complete statement is more than 150000 chars before
+        return {};
+      }
+      // Cut it at the first statement found within 150000 chars before
+      const lastReasonableChunk = beforeCursor.substring(
+        beforeCursor.length - REASONABLE_SURROUNDING_LENGTH
+      );
+      beforeCursor = lastReasonableChunk.substring(lastReasonableChunk.indexOf(';') + 1);
+    }
+
+    if (afterCursor.length > REASONABLE_SURROUNDING_LENGTH) {
+      if (afterCursor.length - afterCursor.indexOf(';') > REASONABLE_SURROUNDING_LENGTH) {
+        // No need to bail out for what's comes after, we can still get keyword completion
+        afterCursor = '';
+      } else {
+        // Cut it at the last statement found within 150000 chars after
+        const firstReasonableChunk = afterCursor.substring(0, REASONABLE_SURROUNDING_LENGTH);
+        afterCursor = firstReasonableChunk.substring(0, firstReasonableChunk.lastIndexOf(';'));
+      }
+    }
+
+    parser.yy.partialLengths = parser.identifyPartials(beforeCursor, afterCursor);
+
+    if (parser.yy.partialLengths.left > 0) {
+      beforeCursor = beforeCursor.substring(0, beforeCursor.length - parser.yy.partialLengths.left);
+    }
+
+    if (parser.yy.partialLengths.right > 0) {
+      afterCursor = afterCursor.substring(parser.yy.partialLengths.right);
+    }
+
+    let result;
+    try {
+      // Add |CURSOR| or |PARTIAL_CURSOR| to represent the different cursor states in the lexer
+      result = parser.parse(
+        beforeCursor +
+          (beforeCursor.length === 0 || /[\s(]$/.test(beforeCursor) ? ' \u2020 ' : '\u2021') +
+          afterCursor
+      );
+    } catch (err) {
+      // On any error try to at least return any existing result
+      if (typeof parser.yy.result === 'undefined') {
+        throw err;
+      }
+      if (debug) {
+        console.warn(err);
+        console.warn(err.stack);
+      }
+      result = parser.yy.result;
+    }
+    if (parser.yy.errors.length > 0) {
+      parser.yy.result.errors = parser.yy.errors;
+      if (debug) {
+        console.warn(parser.yy.errors);
+      }
+    }
+    try {
+      linkTablePrimaries();
+      parser.commitLocations();
+      // Clean up and prioritize
+      adjustForPartialBackticks(parser);
+      prioritizeSuggestions();
+    } catch (err) {
+      if (debug) {
+        console.warn(err);
+        console.warn(err.stack);
+      }
+    }
+
+    parser.yy.allLocations.sort((a, b) => {
+      if (a.location.first_line !== b.location.first_line) {
+        return a.location.first_line - b.location.first_line;
+      }
+      if (a.location.first_column !== b.location.first_column) {
+        return a.location.first_column - b.location.first_column;
+      }
+      if (a.location.last_column !== b.location.last_column) {
+        return b.location.last_column - a.location.last_column;
+      }
+      return b.type.localeCompare(a.type);
+    });
+    parser.yy.result.locations = parser.yy.allLocations;
+    parser.yy.result.definitions = parser.yy.definitions;
+
+    parser.yy.result.locations.forEach(location => {
+      delete location.linked;
+    });
+    if (typeof parser.yy.result.suggestColumns !== 'undefined') {
+      delete parser.yy.result.suggestColumns.linked;
+    }
+
+    SIMPLE_TABLE_REF_SUGGESTIONS.forEach(suggestionType => {
+      if (typeof parser.yy.result[suggestionType] !== 'undefined') {
+        delete parser.yy.result[suggestionType].linked;
+      }
+    });
+
+    if (typeof parser.yy.result.colRef !== 'undefined') {
+      delete parser.yy.result.colRef.linked;
+    }
+    if (typeof parser.yy.result.suggestKeyValues !== 'undefined') {
+      delete parser.yy.result.suggestKeyValues.linked;
+    }
+
+    if (typeof result.error !== 'undefined' && typeof result.error.expected !== 'undefined') {
+      // Remove the cursor from expected tokens
+      result.error.expected = result.error.expected.filter(token => token.indexOf('CURSOR') === -1);
+    }
+
+    if (typeof result.error !== 'undefined' && result.error.recoverable) {
+      delete result.error;
+    }
+
+    // Adjust all the statement locations to include white space surrounding them
+    let lastStatementLocation = null;
+    result.locations.forEach(location => {
+      if (location.type === 'statement') {
+        if (lastStatementLocation === null) {
+          location.location.first_line = 1;
+          location.location.first_column = 1;
+        } else {
+          location.location.first_line = lastStatementLocation.location.last_line;
+          location.location.first_column = lastStatementLocation.location.last_column + 1;
+        }
+        lastStatementLocation = location;
+      }
+    });
+
+    return result;
+  };
+};
+
+export default {
+  initSqlParser: initSqlParser,
+  initSyntaxParser: initSyntaxParser
+};

+ 6 - 2
desktop/core/src/desktop/js/parse/sql/sqlParserRepository.ts

@@ -29,7 +29,9 @@ const AUTOCOMPLETE_MODULES = {
   ksql: () => import(/* webpackChunkName: "ksql-parser" */ './ksql/ksqlAutocompleteParser'),
   phoenix: () =>
     import(/* webpackChunkName: "phoenix-parser" */ './phoenix/phoenixAutocompleteParser'),
-  presto: () => import(/* webpackChunkName: "presto-parser" */ './presto/prestoAutocompleteParser')
+  presto: () => import(/* webpackChunkName: "presto-parser" */ './presto/prestoAutocompleteParser'),
+  sparksql: () =>
+    import(/* webpackChunkName: "sparksql-parser" */ './sparksql/sparksqlAutocompleteParser')
 };
 const SYNTAX_MODULES = {
   calcite: () => import(/* webpackChunkName: "calcite-parser" */ './calcite/calciteSyntaxParser'),
@@ -40,7 +42,9 @@ const SYNTAX_MODULES = {
   impala: () => import(/* webpackChunkName: "impala-parser" */ './impala/impalaSyntaxParser'),
   ksql: () => import(/* webpackChunkName: "ksql-parser" */ './ksql/ksqlSyntaxParser'),
   phoenix: () => import(/* webpackChunkName: "phoenix-parser" */ './phoenix/phoenixSyntaxParser'),
-  presto: () => import(/* webpackChunkName: "presto-parser" */ './presto/prestoSyntaxParser')
+  presto: () => import(/* webpackChunkName: "presto-parser" */ './presto/prestoSyntaxParser'),
+  sparksql: () =>
+    import(/* webpackChunkName: "sparksql-parser" */ './sparksql/sparksqlSyntaxParser')
 };
 
 export class SqlParserRepository implements SqlParserProvider {

Some files were not shown because too many files changed in this diff