Browse Source

HUE-9077 [kafka] Adding new parser skeleton

Romain 6 years ago
parent
commit
3118289e4e
31 changed files with 16410 additions and 2 deletions
  1. 19 0
      desktop/core/src/desktop/js/parse/jison/sql/ksql/autocomplete_footer.jison
  2. 29 0
      desktop/core/src/desktop/js/parse/jison/sql/ksql/autocomplete_header.jison
  3. 226 0
      desktop/core/src/desktop/js/parse/jison/sql/ksql/sql.jisonlex
  4. 109 0
      desktop/core/src/desktop/js/parse/jison/sql/ksql/sql_alter.jison
  5. 615 0
      desktop/core/src/desktop/js/parse/jison/sql/ksql/sql_create.jison
  6. 184 0
      desktop/core/src/desktop/js/parse/jison/sql/ksql/sql_drop.jison
  7. 132 0
      desktop/core/src/desktop/js/parse/jison/sql/ksql/sql_error.jison
  8. 72 0
      desktop/core/src/desktop/js/parse/jison/sql/ksql/sql_insert.jison
  9. 2762 0
      desktop/core/src/desktop/js/parse/jison/sql/ksql/sql_main.jison
  10. 46 0
      desktop/core/src/desktop/js/parse/jison/sql/ksql/sql_set.jison
  11. 122 0
      desktop/core/src/desktop/js/parse/jison/sql/ksql/sql_update.jison
  12. 39 0
      desktop/core/src/desktop/js/parse/jison/sql/ksql/sql_use.jison
  13. 839 0
      desktop/core/src/desktop/js/parse/jison/sql/ksql/sql_valueExpression.jison
  14. 19 0
      desktop/core/src/desktop/js/parse/jison/sql/ksql/syntax_footer.jison
  15. 28 0
      desktop/core/src/desktop/js/parse/jison/sql/ksql/syntax_header.jison
  16. 91 0
      desktop/core/src/desktop/js/parse/sql/ksql/ksqlAutocompleteParser.js
  17. 91 0
      desktop/core/src/desktop/js/parse/sql/ksql/ksqlSyntaxParser.js
  18. 376 0
      desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParserSpec.js
  19. 157 0
      desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParser_Alter_Spec.js
  20. 277 0
      desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParser_Create_Spec.js
  21. 290 0
      desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParser_Drop_Spec.js
  22. 138 0
      desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParser_Error_Spec.js
  23. 139 0
      desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParser_Insert_Spec.js
  24. 423 0
      desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParser_Locations_Spec.js
  25. 6174 0
      desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParser_Select_Spec.js
  26. 50 0
      desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParser_Set_Spec.js
  27. 384 0
      desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParser_Update_Spec.js
  28. 146 0
      desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParser_Use_Spec.js
  29. 208 0
      desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlSyntaxParserSpec.js
  30. 2221 0
      desktop/core/src/desktop/js/parse/sql/ksql/sqlParseSupport.js
  31. 4 2
      desktop/core/src/desktop/js/parse/sql/sqlParserRepository.js

+ 19 - 0
desktop/core/src/desktop/js/parse/jison/sql/ksql/autocomplete_footer.jison

@@ -0,0 +1,19 @@
+// 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.
+
+%%
+
+SqlParseSupport.initSqlParser(parser);

+ 29 - 0
desktop/core/src/desktop/js/parse/jison/sql/ksql/autocomplete_header.jison

@@ -0,0 +1,29 @@
+// 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.
+
+%left 'AND' 'OR'
+%left 'BETWEEN'
+%left 'NOT' '!' '~'
+%left '=' '<' '>' 'COMPARISON_OPERATOR'
+%left '-' '*' 'ARITHMETIC_OPERATOR'
+
+%left ';' ','
+%nonassoc 'CURSOR' 'PARTIAL_CURSOR'
+%nonassoc 'IN' 'IS' 'LIKE' 'RLIKE' 'REGEXP' 'EXISTS' NEGATION
+
+%start SqlAutocomplete
+
+%%

+ 226 - 0
desktop/core/src/desktop/js/parse/jison/sql/ksql/sql.jisonlex

@@ -0,0 +1,226 @@
+// 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'; }
+'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'; }
+'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'; }
+'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>.                       { }

+ 109 - 0
desktop/core/src/desktop/js/parse/jison/sql/ksql/sql_alter.jison

@@ -0,0 +1,109 @@
+// 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.
+
+DataDefinition
+ : AlterStatement
+ ;
+
+DataDefinition_EDIT
+ : AlterStatement_EDIT
+ ;
+
+AlterStatement
+ : AlterTable
+ | AlterView
+ ;
+
+AlterStatement_EDIT
+ : AlterTable_EDIT
+ | AlterView_EDIT
+ | 'ALTER' 'CURSOR'
+   {
+     parser.suggestKeywords(['TABLE', 'VIEW']);
+   }
+ ;
+
+AlterTable
+ : AlterTableLeftSide PartitionSpec
+ ;
+
+AlterTable_EDIT
+ : AlterTableLeftSide_EDIT
+ | AlterTableLeftSide_EDIT PartitionSpec
+ | AlterTableLeftSide 'CURSOR'
+ | AlterTableLeftSide PartitionSpec 'CURSOR'
+ ;
+
+AlterTableLeftSide
+ : 'ALTER' 'TABLE' SchemaQualifiedTableIdentifier
+   {
+     parser.addTablePrimary($3);
+   }
+ ;
+
+AlterTableLeftSide_EDIT
+ : 'ALTER' 'TABLE' SchemaQualifiedTableIdentifier_EDIT
+   {
+     if (parser.yy.result.suggestTables) {
+       parser.yy.result.suggestTables.onlyTables = true;
+     }
+   }
+ | 'ALTER' 'TABLE' 'CURSOR'
+   {
+     parser.suggestTables({ onlyTables: true });
+     parser.suggestDatabases({ appendDot: true });
+   }
+ ;
+
+AlterView
+ : AlterViewLeftSide 'AS' QuerySpecification
+ ;
+
+AlterView_EDIT
+ : AlterViewLeftSide_EDIT
+ | AlterViewLeftSide 'CURSOR'
+   {
+     parser.suggestKeywords(['AS']);
+   }
+ | AlterViewLeftSide 'SET' 'CURSOR'
+ | AlterViewLeftSide 'AS' 'CURSOR'
+   {
+     parser.suggestKeywords(['SELECT']);
+   }
+ | AlterViewLeftSide 'AS' QuerySpecification_EDIT
+ ;
+
+
+AlterViewLeftSide
+ : 'ALTER' 'VIEW' SchemaQualifiedTableIdentifier
+   {
+     parser.addTablePrimary($3);
+   }
+ ;
+
+AlterViewLeftSide_EDIT
+ : 'ALTER' 'VIEW' SchemaQualifiedTableIdentifier_EDIT
+   {
+     if (parser.yy.result.suggestTables) {
+       parser.yy.result.suggestTables.onlyViews = true;
+     }
+   }
+ | 'ALTER' 'VIEW' 'CURSOR'
+   {
+     parser.suggestTables({ onlyViews: true });
+     parser.suggestDatabases({ appendDot: true });
+   }
+ ;

+ 615 - 0
desktop/core/src/desktop/js/parse/jison/sql/ksql/sql_create.jison

@@ -0,0 +1,615 @@
+// 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.
+
+DataDefinition
+ : CreateStatement
+ ;
+
+DataDefinition_EDIT
+ : CreateStatement_EDIT
+ ;
+
+CreateStatement
+ : DatabaseDefinition
+ | TableDefinition
+ | ViewDefinition
+ | RoleDefinition
+ ;
+
+CreateStatement_EDIT
+ : DatabaseDefinition_EDIT
+ | TableDefinition_EDIT
+ | ViewDefinition_EDIT
+ | 'CREATE' 'CURSOR'
+   {
+     parser.suggestKeywords(['DATABASE', 'ROLE', 'SCHEMA', 'TABLE', 'VIEW']);
+   }
+ ;
+
+DatabaseDefinition
+ : 'CREATE' DatabaseOrSchema OptionalIfNotExists
+ | 'CREATE' DatabaseOrSchema OptionalIfNotExists RegularIdentifier DatabaseDefinitionOptionals
+   {
+     parser.addNewDatabaseLocation(@4, [{ name: $4 }]);
+   }
+ ;
+
+DatabaseDefinition_EDIT
+ : 'CREATE' DatabaseOrSchema OptionalIfNotExists 'CURSOR'
+   {
+     if (!$3) {
+       parser.suggestKeywords(['IF NOT EXISTS']);
+     }
+   }
+ | 'CREATE' DatabaseOrSchema OptionalIfNotExists_EDIT
+ | 'CREATE' DatabaseOrSchema OptionalIfNotExists 'CURSOR' RegularIdentifier
+   {
+     if (!$3) {
+       parser.suggestKeywords(['IF NOT EXISTS']);
+     }
+     parser.addNewDatabaseLocation(@5, [{ name: $5 }]);
+   }
+ | 'CREATE' DatabaseOrSchema OptionalIfNotExists_EDIT RegularIdentifier
+   {
+     parser.addNewDatabaseLocation(@4, [{ name: $4 }]);
+   }
+ | 'CREATE' DatabaseOrSchema OptionalIfNotExists RegularIdentifier DatabaseDefinitionOptionals 'CURSOR'
+   {
+     parser.addNewDatabaseLocation(@4, [{ name: $4 }]);
+   }
+ ;
+
+DatabaseDefinitionOptionals
+ : OptionalComment
+   {
+     if (!$1) {
+       parser.suggestKeywords(['COMMENT']);
+     }
+   }
+ ;
+
+DatabaseDefinitionOptionals_EDIT
+ : OptionalComment_INVALID
+ ;
+
+OptionalComment
+ :
+ | Comment
+ ;
+
+Comment
+ : 'COMMENT' QuotedValue
+ ;
+
+OptionalComment_INVALID
+ : Comment_INVALID
+ ;
+
+Comment_INVALID
+ : 'COMMENT' SINGLE_QUOTE
+ | 'COMMENT' DOUBLE_QUOTE
+ | 'COMMENT' SINGLE_QUOTE VALUE
+ | 'COMMENT' DOUBLE_QUOTE VALUE
+ ;
+
+TableDefinition
+ : 'CREATE' 'TABLE' OptionalIfNotExists TableDefinitionRightPart
+ ;
+
+TableDefinition_EDIT
+ : 'CREATE' 'TABLE' OptionalIfNotExists TableDefinitionRightPart_EDIT
+ | 'CREATE' 'TABLE' OptionalIfNotExists 'CURSOR'
+   {
+     if (!$3) {
+       parser.suggestKeywords(['IF NOT EXISTS']);
+     }
+   }
+ | 'CREATE' 'TABLE' OptionalIfNotExists_EDIT
+ ;
+
+TableDefinitionRightPart
+ : TableIdentifierAndOptionalColumnSpecification OptionalPartitionedBy OptionalAsSelectStatement
+ ;
+
+TableDefinitionRightPart_EDIT
+ : TableIdentifierAndOptionalColumnSpecification_EDIT OptionalPartitionedBy OptionalAsSelectStatement
+ | TableIdentifierAndOptionalColumnSpecification PartitionedBy_EDIT OptionalAsSelectStatement
+ | TableIdentifierAndOptionalColumnSpecification OptionalPartitionedBy OptionalAsSelectStatement_EDIT
+ | TableIdentifierAndOptionalColumnSpecification OptionalPartitionedBy 'CURSOR'
+   {
+     var keywords = [];
+     if (!$1 && !$2) {
+       keywords.push({ value: 'LIKE', weight: 1 });
+     } else {
+       if (!$2) {
+         keywords.push({ value: 'PARTITIONED BY', weight: 12 });
+       }
+       keywords.push({ value: 'AS', weight: 1 });
+     }
+
+     if (keywords.length > 0) {
+       parser.suggestKeywords(keywords);
+     }
+   }
+ ;
+
+TableIdentifierAndOptionalColumnSpecification
+ : SchemaQualifiedIdentifier OptionalColumnSpecificationsOrLike
+   {
+     parser.addNewTableLocation(@1, $1, $2);
+     $$ = $2;
+   }
+ ;
+
+TableIdentifierAndOptionalColumnSpecification_EDIT
+ : SchemaQualifiedIdentifier OptionalColumnSpecificationsOrLike_EDIT
+ | SchemaQualifiedIdentifier_EDIT OptionalColumnSpecificationsOrLike
+ ;
+
+OptionalColumnSpecificationsOrLike
+ :
+ | ParenthesizedColumnSpecificationList
+ | 'LIKE' SchemaQualifiedTableIdentifier    -> []
+ ;
+
+OptionalColumnSpecificationsOrLike_EDIT
+ : ParenthesizedColumnSpecificationList_EDIT
+ | 'LIKE' 'CURSOR'
+   {
+     parser.suggestTables();
+     parser.suggestDatabases({ appendDot: true });
+   }
+ | 'LIKE' SchemaQualifiedTableIdentifier_EDIT
+ ;
+
+ParenthesizedColumnSpecificationList
+ : '(' ColumnSpecificationList ')'                              -> $2
+ ;
+
+ParenthesizedColumnSpecificationList_EDIT
+ : '(' ColumnSpecificationList_EDIT RightParenthesisOrError
+ ;
+
+ColumnSpecificationList
+ : ColumnSpecification                              -> [$1]
+ | ColumnSpecificationList ',' ColumnSpecification  -> $1.concat($3)
+ ;
+
+ColumnSpecificationList_EDIT
+ : ColumnSpecification_EDIT
+ | ColumnSpecification_EDIT ',' ColumnSpecificationList
+ | ColumnSpecificationList ',' ColumnSpecification_EDIT
+ | ColumnSpecificationList ',' ColumnSpecification_EDIT ',' ColumnSpecificationList
+ | ColumnSpecification 'CURSOR'
+   {
+     parser.checkForKeywords($1);
+   }
+ | ColumnSpecification 'CURSOR' ',' ColumnSpecificationList
+   {
+     parser.checkForKeywords($1);
+   }
+ | ColumnSpecificationList ',' ColumnSpecification 'CURSOR'
+   {
+     parser.checkForKeywords($3);
+   }
+ | ColumnSpecificationList ',' ColumnSpecification 'CURSOR' ',' ColumnSpecificationList
+   {
+     parser.checkForKeywords($3);
+   }
+ ;
+
+ColumnSpecification
+ : ColumnIdentifier ColumnDataType OptionalColumnOptions
+   {
+     $$ = $1;
+     $$.type = $2;
+     var keywords = [];
+     if (!$3['comment']) {
+       keywords.push('COMMENT');
+     }
+     if (keywords.length > 0) {
+       $$.suggestKeywords = keywords;
+     }
+   }
+ ;
+
+ColumnSpecification_EDIT
+ : ColumnIdentifier 'CURSOR' OptionalColumnOptions
+   {
+     parser.suggestKeywords(parser.getColumnDataTypeKeywords());
+   }
+ | ColumnIdentifier ColumnDataType_EDIT OptionalColumnOptions
+ | ColumnIdentifier ColumnDataType ColumnOptions_EDIT
+ ;
+
+OptionalColumnOptions
+ :                      -> {}
+ | ColumnOptions
+ ;
+
+ColumnOptions
+ : ColumnOption
+   {
+     $$ = {};
+     $$[$1] = true;
+   }
+ | ColumnOptions ColumnOption
+   {
+     $1[$2] = true;
+   }
+ ;
+
+ColumnOptions_EDIT
+ : ColumnOption_EDIT
+ | ColumnOption_EDIT ColumnOptions
+ | ColumnOptions ColumnOption_EDIT
+ | ColumnOptions ColumnOption_EDIT ColumnOptions
+ ;
+
+ColumnOption
+ : 'NOT' 'NULL'                                              -> 'null'
+ | 'NULL'                                                    -> 'null'
+ | Comment                                                   -> 'comment'
+ ;
+
+ColumnOption_EDIT
+ : 'NOT' 'CURSOR'
+   {
+     parser.suggestKeywords(['NULL']);
+   }
+ ;
+
+ColumnDataType
+ : PrimitiveType
+ | ArrayType
+ | MapType
+ | StructType
+ | ArrayType_INVALID
+ | MapType_INVALID
+ | StructType_INVALID
+ ;
+
+ColumnDataType_EDIT
+ : ArrayType_EDIT
+ | MapType_EDIT
+ | StructType_EDIT
+ ;
+
+ArrayType
+ : 'ARRAY' '<' ColumnDataType '>'
+ ;
+
+ArrayType_INVALID
+ : 'ARRAY' '<' '>'
+ ;
+
+ArrayType_EDIT
+ : 'ARRAY' '<' AnyCursor GreaterThanOrError
+   {
+     parser.suggestKeywords(parser.getColumnDataTypeKeywords());
+   }
+ | 'ARRAY' '<' ColumnDataType_EDIT GreaterThanOrError
+ ;
+
+MapType
+ : 'MAP' '<' PrimitiveType ',' ColumnDataType '>'
+ ;
+
+MapType_INVALID
+ : 'MAP' '<' '>'
+ ;
+
+MapType_EDIT
+ : 'MAP' '<' PrimitiveType ',' ColumnDataType_EDIT GreaterThanOrError
+ | 'MAP' '<' AnyCursor GreaterThanOrError
+   {
+     parser.suggestKeywords(parser.getTypeKeywords());
+   }
+ | 'MAP' '<' PrimitiveType ',' AnyCursor GreaterThanOrError
+   {
+     parser.suggestKeywords(parser.getColumnDataTypeKeywords());
+   }
+ | 'MAP' '<' ',' AnyCursor GreaterThanOrError
+   {
+     parser.suggestKeywords(parser.getColumnDataTypeKeywords());
+   }
+ ;
+
+StructType
+ : 'STRUCT' '<' StructDefinitionList '>'
+ ;
+
+StructType_INVALID
+ : 'STRUCT' '<' '>'
+ ;
+
+StructType_EDIT
+ : 'STRUCT' '<' StructDefinitionList_EDIT GreaterThanOrError
+ ;
+
+StructDefinitionList
+ : StructDefinition
+ | StructDefinitionList ',' StructDefinition
+ ;
+
+StructDefinitionList_EDIT
+ : StructDefinition_EDIT
+ | StructDefinition_EDIT Commas
+ | StructDefinition_EDIT Commas StructDefinitionList
+ | StructDefinitionList ',' StructDefinition_EDIT
+ | StructDefinitionList ',' StructDefinition_EDIT Commas StructDefinitionList
+ ;
+
+StructDefinition
+ : RegularOrBacktickedIdentifier ':' ColumnDataType OptionalComment
+ ;
+
+StructDefinition_EDIT
+ : Commas RegularOrBacktickedIdentifier ':' ColumnDataType 'CURSOR'
+   {
+     parser.suggestKeywords(['COMMENT']);
+   }
+ | Commas RegularOrBacktickedIdentifier ':' AnyCursor
+   {
+     parser.suggestKeywords(parser.getColumnDataTypeKeywords());
+   }
+ | Commas RegularOrBacktickedIdentifier ':' ColumnDataType_EDIT
+ | RegularOrBacktickedIdentifier ':' ColumnDataType 'CURSOR'
+   {
+     parser.suggestKeywords(['COMMENT']);
+   }
+ | RegularOrBacktickedIdentifier ':' AnyCursor
+   {
+     parser.suggestKeywords(parser.getColumnDataTypeKeywords());
+   }
+ | RegularOrBacktickedIdentifier ':' ColumnDataType_EDIT
+ ;
+
+ColumnDataTypeList
+ : ColumnDataType
+ | ColumnDataTypeList ',' ColumnDataType
+ ;
+
+ColumnDataTypeList_EDIT
+ : ColumnDataTypeListInner_EDIT
+ | ColumnDataTypeListInner_EDIT Commas
+ | ColumnDataTypeList ',' ColumnDataTypeListInner_EDIT
+ | ColumnDataTypeListInner_EDIT Commas ColumnDataTypeList
+ | ColumnDataTypeList ',' ColumnDataTypeListInner_EDIT Commas ColumnDataTypeList
+ ;
+
+ColumnDataTypeListInner_EDIT
+ : Commas AnyCursor
+   {
+     parser.suggestKeywords(parser.getColumnDataTypeKeywords());
+   }
+ | Commas ColumnDataType_EDIT
+ | AnyCursor
+   {
+     parser.suggestKeywords(parser.getColumnDataTypeKeywords());
+   }
+ | ColumnDataType_EDIT
+ ;
+
+GreaterThanOrError
+ : '>'
+ | error
+ ;
+
+OptionalPartitionedBy
+ :
+ | PartitionedBy
+ ;
+
+PartitionedBy
+ : 'PARTITION' 'BY' RangeClause
+ ;
+
+PartitionedBy_EDIT
+ : 'PARTITION' 'CURSOR'
+   {
+     parser.suggestKeywords(['BY']);
+   }
+ | 'PARTITION' 'BY' 'CURSOR'
+   {
+     parser.suggestKeywords(['RANGE']);
+   }
+ | 'PARTITION' 'BY' RangeClause_EDIT
+ ;
+
+RangeClause
+ : 'RANGE' ParenthesizedColumnList ParenthesizedPartitionValuesList
+ ;
+
+RangeClause_EDIT
+ : 'RANGE' 'CURSOR'
+ | 'RANGE' ParenthesizedColumnList_EDIT
+ | 'RANGE' ParenthesizedColumnList 'CURSOR'
+ | 'RANGE' ParenthesizedColumnList ParenthesizedPartitionValuesList_EDIT
+ | 'RANGE' ParenthesizedColumnList_EDIT ParenthesizedPartitionValuesList
+ ;
+
+ParenthesizedPartitionValuesList
+ : '(' PartitionValueList ')'
+ ;
+
+ParenthesizedPartitionValuesList_EDIT
+ : '(' 'CURSOR' RightParenthesisOrError
+   {
+     parser.suggestKeywords(['PARTITION']);
+   }
+ |'(' PartitionValueList_EDIT RightParenthesisOrError
+ ;
+
+PartitionValueList
+ : PartitionValue
+ | PartitionValueList ',' PartitionValue
+ ;
+
+PartitionValueList_EDIT
+ : PartitionValue_EDIT
+ | PartitionValueList ',' 'CURSOR'
+   {
+     parser.suggestKeywords(['PARTITION']);
+   }
+ | PartitionValueList ',' 'CURSOR' ',' PartitionValueList
+   {
+     parser.suggestKeywords(['PARTITION']);
+   }
+ | PartitionValueList ',' PartitionValue_EDIT
+ | PartitionValueList ',' PartitionValue_EDIT ',' PartitionValueList
+ ;
+
+PartitionValue
+ : 'PARTITION' ValueExpression LessThanOrEqualTo 'VALUES' LessThanOrEqualTo ValueExpression
+ | 'PARTITION' 'VALUES' LessThanOrEqualTo ValueExpression
+ | 'PARTITION' ValueExpression LessThanOrEqualTo 'VALUES'
+ ;
+
+PartitionValue_EDIT
+ : 'PARTITION' 'CURSOR'
+   {
+     parser.suggestKeywords(['VALUE', 'VALUES']);
+   }
+ | 'PARTITION' ValueExpression_EDIT
+   {
+     if ($2.endsWithLessThanOrEqual) {
+      parser.suggestKeywords(['VALUES']);
+     }
+   }
+ | 'PARTITION' ValueExpression 'CURSOR'
+   {
+     parser.suggestKeywords(['<', '<=']);
+   }
+ | 'PARTITION' ValueExpression LessThanOrEqualTo 'CURSOR'
+   {
+     parser.suggestKeywords(['VALUES']);
+   }
+ | 'PARTITION' ValueExpression_EDIT LessThanOrEqualTo 'VALUES'
+ | 'PARTITION' ValueExpression LessThanOrEqualTo 'VALUES' 'CURSOR'
+   {
+     parser.suggestKeywords(['<', '<=']);
+   }
+ | 'PARTITION' ValueExpression LessThanOrEqualTo 'VALUES' LessThanOrEqualTo 'CURSOR'
+   {
+     parser.suggestFunctions();
+   }
+ | 'PARTITION' ValueExpression LessThanOrEqualTo 'VALUES' LessThanOrEqualTo ValueExpression_EDIT
+ | 'PARTITION' 'VALUES' 'CURSOR'
+   {
+     parser.suggestKeywords(['<', '<=']);
+   }
+ | 'PARTITION' 'VALUES' LessThanOrEqualTo 'CURSOR'
+   {
+     parser.suggestFunctions();
+   }
+ | 'PARTITION' 'VALUES' LessThanOrEqualTo ValueExpression_EDIT
+ ;
+
+LessThanOrEqualTo
+ : '<'
+ | 'COMPARISON_OPERATOR' // This is fine for autocompletion
+ ;
+
+OptionalAsSelectStatement
+ :
+ | 'AS' CommitLocations QuerySpecification
+ ;
+
+OptionalAsSelectStatement_EDIT
+ : 'AS' CommitLocations 'CURSOR'
+   {
+     parser.suggestKeywords(['SELECT']);
+   }
+ | 'AS' CommitLocations QuerySpecification_EDIT
+ ;
+
+CommitLocations
+ : /* empty */
+   {
+     parser.commitLocations();
+   }
+ ;
+
+ViewDefinition
+ : 'CREATE' 'VIEW' OptionalIfNotExists SchemaQualifiedIdentifier OptionalParenthesizedViewColumnList OptionalComment 'AS' QuerySpecification
+ ;
+
+ViewDefinition_EDIT
+ : 'CREATE' 'VIEW' OptionalIfNotExists 'CURSOR'
+   {
+     if (!$3) {
+       parser.suggestKeywords(['IF NOT EXISTS']);
+     }
+     parser.suggestDatabases({ appendDot: true });
+   }
+ | 'CREATE' 'VIEW' OptionalIfNotExists 'CURSOR' SchemaQualifiedIdentifier OptionalParenthesizedViewColumnList OptionalComment 'AS' QuerySpecification
+   {
+     if (!$3) {
+       parser.suggestKeywords(['IF NOT EXISTS']);
+     }
+   }
+ | 'CREATE' 'VIEW' OptionalIfNotExists_EDIT
+ | 'CREATE' 'VIEW' OptionalIfNotExists SchemaQualifiedIdentifier ParenthesizedViewColumnList_EDIT OptionalComment
+ | 'CREATE' 'VIEW' OptionalIfNotExists SchemaQualifiedIdentifier OptionalParenthesizedViewColumnList OptionalComment 'CURSOR'
+   {
+     var keywords = [{value: 'AS', weight: 1 }];
+     if (!$6) {
+       keywords.push({ value: 'COMMENT', weight: 3 });
+     }
+     parser.suggestKeywords(keywords);
+   }
+ | 'CREATE' 'VIEW' OptionalIfNotExists SchemaQualifiedIdentifier OptionalParenthesizedViewColumnList OptionalComment 'AS' 'CURSOR'
+   {
+     parser.suggestKeywords(['SELECT']);
+   }
+ | 'CREATE' 'VIEW' OptionalIfNotExists SchemaQualifiedIdentifier OptionalParenthesizedViewColumnList OptionalComment 'AS' QuerySpecification_EDIT
+ | 'CREATE' 'VIEW' OptionalIfNotExists SchemaQualifiedIdentifier_EDIT OptionalParenthesizedViewColumnList OptionalComment 'AS' QuerySpecification
+ ;
+
+OptionalParenthesizedViewColumnList
+ :
+ | ParenthesizedViewColumnList
+ ;
+
+ParenthesizedViewColumnList
+ : '(' ViewColumnList ')'
+ ;
+
+ParenthesizedViewColumnList_EDIT
+ : '(' ViewColumnList_EDIT RightParenthesisOrError
+   {
+     if (!$2) {
+       parser.suggestKeywords(['COMMENT']);
+     }
+   }
+ ;
+
+ViewColumnList
+ : ColumnReference OptionalComment
+ | ViewColumnList ',' ColumnReference OptionalComment
+ ;
+
+ViewColumnList_EDIT
+ : ColumnReference OptionalComment 'CURSOR'                                        -> $2
+ | ColumnReference OptionalComment 'CURSOR' ',' ViewColumnList                     -> $2
+ | ViewColumnList ',' ColumnReference OptionalComment 'CURSOR'                     -> $4
+ | ViewColumnList ',' ColumnReference OptionalComment 'CURSOR' ',' ViewColumnList  -> $4
+ ;
+
+RoleDefinition
+ : 'CREATE' 'ROLE' RegularIdentifier
+ ;

+ 184 - 0
desktop/core/src/desktop/js/parse/jison/sql/ksql/sql_drop.jison

@@ -0,0 +1,184 @@
+// 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.
+
+DataDefinition
+ : DropStatement
+ ;
+
+DataDefinition_EDIT
+ : DropStatement_EDIT
+ ;
+
+DropStatement
+ : DropDatabaseStatement
+ | DropRoleStatement
+ | DropTableStatement
+ | DropViewStatement
+ | TruncateTableStatement
+ ;
+
+DropStatement_EDIT
+ : DropDatabaseStatement_EDIT
+ | DropTableStatement_EDIT
+ | DropViewStatement_EDIT
+ | TruncateTableStatement_EDIT
+ | 'DROP' 'CURSOR'
+   {
+     parser.suggestKeywords(['DATABASE', 'ROLE', 'SCHEMA', 'TABLE', 'VIEW']);
+   }
+ ;
+
+DropDatabaseStatement
+ : 'DROP' DatabaseOrSchema OptionalIfExists RegularOrBacktickedIdentifier OptionalCascade
+ ;
+
+DropDatabaseStatement_EDIT
+ : 'DROP' DatabaseOrSchema OptionalIfExists
+ | 'DROP' DatabaseOrSchema OptionalIfExists_EDIT
+ | 'DROP' DatabaseOrSchema OptionalIfExists 'CURSOR'
+   {
+     if (!$3) {
+       parser.suggestKeywords(['IF EXISTS']);
+     }
+     parser.suggestDatabases();
+   }
+ | 'DROP' DatabaseOrSchema OptionalIfExists RegularOrBacktickedIdentifier 'CURSOR'
+   {
+     parser.suggestKeywords(['CASCADE']);
+   }
+ | 'DROP' DatabaseOrSchema OptionalIfExists_EDIT RegularOrBacktickedIdentifier OptionalCascade
+ | 'DROP' DatabaseOrSchema OptionalIfExists 'CURSOR' RegularOrBacktickedIdentifier OptionalCascade
+   {
+     if (!$3) {
+       parser.suggestKeywords(['IF EXISTS']);
+     }
+   }
+ ;
+DropRoleStatement
+ : 'DROP' 'ROLE' RegularIdentifier
+ ;
+
+DropTableStatement
+ : 'DROP' 'TABLE' OptionalIfExists SchemaQualifiedTableIdentifier OptionalPurge
+   {
+     parser.addTablePrimary($4);
+   }
+ ;
+
+DropTableStatement_EDIT
+ : 'DROP' 'TABLE' OptionalIfExists_EDIT
+ | 'DROP' 'TABLE' OptionalIfExists 'CURSOR'
+   {
+     if (!$3) {
+       parser.suggestKeywords(['IF EXISTS']);
+     }
+     parser.suggestTables({ onlyTables: true });
+     parser.suggestDatabases({
+       appendDot: true
+     });
+   }
+ | 'DROP' 'TABLE' OptionalIfExists SchemaQualifiedTableIdentifier_EDIT OptionalPurge
+   {
+     if (parser.yy.result.suggestTables) {
+       parser.yy.result.suggestTables.onlyTables = true;
+     }
+   }
+ | 'DROP' 'TABLE' OptionalIfExists_EDIT SchemaQualifiedTableIdentifier OptionalPurge
+ | 'DROP' 'TABLE' OptionalIfExists SchemaQualifiedTableIdentifier OptionalPurge 'CURSOR'
+   {
+     parser.addTablePrimary($4);
+     if (!$5) {
+       parser.suggestKeywords(['PURGE']);
+     }
+   }
+ ;
+
+OptionalPurge
+ :
+ | 'PURGE'
+ ;
+
+DropViewStatement
+ : 'DROP' 'VIEW' OptionalIfExists SchemaQualifiedTableIdentifier
+   {
+     parser.addTablePrimary($4);
+   }
+ ;
+
+DropViewStatement_EDIT
+ : 'DROP' 'VIEW' OptionalIfExists 'CURSOR'
+   {
+     if (!$3) {
+       parser.suggestKeywords(['IF EXISTS']);
+     }
+     parser.suggestTables({ onlyViews: true });
+     parser.suggestDatabases({ appendDot: true });
+   }
+ | 'DROP' 'VIEW' OptionalIfExists 'CURSOR' SchemaQualifiedTableIdentifier
+   {
+     parser.addTablePrimary($5);
+     if (!$3) {
+       parser.suggestKeywords(['IF EXISTS']);
+     }
+   }
+ | 'DROP' 'VIEW' OptionalIfExists_EDIT
+ | 'DROP' 'VIEW' OptionalIfExists_EDIT SchemaQualifiedTableIdentifier
+   {
+     parser.addTablePrimary($4);
+   }
+ | 'DROP' 'VIEW' OptionalIfExists SchemaQualifiedTableIdentifier_EDIT
+   {
+     if (parser.yy.result.suggestTables) {
+       parser.yy.result.suggestTables.onlyViews = true;
+     }
+   }
+ ;
+
+TruncateTableStatement
+ : 'TRUNCATE' 'TABLE' OptionalIfExists SchemaQualifiedTableIdentifier
+   {
+     parser.addTablePrimary($4);
+   }
+ ;
+
+TruncateTableStatement_EDIT
+ : 'TRUNCATE' 'CURSOR'
+   {
+     parser.suggestKeywords(['TABLE']);
+   }
+ | 'TRUNCATE' 'TABLE' OptionalIfExists 'CURSOR'
+   {
+     parser.suggestTables();
+     parser.suggestDatabases({ appendDot: true });
+     if (!$3) {
+       parser.suggestKeywords(['IF EXISTS']);
+     }
+   }
+ | 'TRUNCATE' 'TABLE' OptionalIfExists_EDIT
+ | 'TRUNCATE' 'TABLE' OptionalIfExists SchemaQualifiedTableIdentifier_EDIT
+ | 'TRUNCATE' 'TABLE' OptionalIfExists SchemaQualifiedTableIdentifier 'CURSOR'
+   {
+     parser.addTablePrimary($4);
+   }
+ | 'TRUNCATE' 'TABLE' OptionalIfExists 'CURSOR' SchemaQualifiedTableIdentifier
+   {
+     parser.addTablePrimary($4);
+     if (!$3) {
+       parser.suggestKeywords(['IF EXISTS']);
+     }
+   }
+ | 'TRUNCATE' 'TABLE' OptionalIfExists_EDIT SchemaQualifiedTableIdentifier OptionalPartitionSpec
+ ;

+ 132 - 0
desktop/core/src/desktop/js/parse/jison/sql/ksql/sql_error.jison

@@ -0,0 +1,132 @@
+// 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.
+
+SqlStatements
+ : error
+ | NonStartingToken error // Having just ': error' does not work for some reason, jison bug?
+ ;
+
+SqlStatement_EDIT
+ : AnyCursor error
+   {
+     parser.suggestDdlAndDmlKeywords();
+   }
+ ;
+
+SelectStatement
+ : 'SELECT' OptionalAllOrDistinct SelectList_ERROR TableExpression
+ | 'SELECT' OptionalAllOrDistinct SelectList TableExpression_ERROR
+ ;
+
+SelectStatement_EDIT
+ : 'SELECT' OptionalAllOrDistinct SelectList_ERROR_EDIT TableExpression
+   {
+     parser.selectListNoTableSuggest($3, $2);
+   }
+ | 'SELECT' OptionalAllOrDistinct SelectList_ERROR TableExpression_EDIT
+ ;
+
+SelectList_ERROR
+ : ErrorList
+ | SelectList ',' ErrorList
+ | ErrorList ',' SelectList ',' ErrorList
+ | ErrorList ',' SelectList
+ | SelectList ',' ErrorList ',' SelectList
+ ;
+
+SelectList_ERROR_EDIT
+ : ErrorList ',' SelectList_EDIT                               -> $3
+ | SelectList ',' ErrorList ',' SelectList_EDIT                -> $5
+ | ErrorList ',' SelectList ',' ErrorList ',' SelectList_EDIT  -> $7
+ | ErrorList ',' AnyCursor
+   {
+     $$ = { cursorAtStart : false, suggestFunctions: true, suggestColumns: true, suggestAggregateFunctions: true };
+   }
+ | SelectList ',' ErrorList ',' AnyCursor
+   {
+     $$ = { cursorAtStart : false, suggestFunctions: true, suggestColumns: true, suggestAggregateFunctions: true };
+   }
+ | ErrorList ',' SelectList ',' Errors ',' AnyCursor
+   {
+     $$ = { cursorAtStart : true, suggestFunctions: true, suggestColumns: true, suggestAggregateFunctions: true };
+   }
+ ;
+
+SetSpecification
+ : 'SET' SetOption '=' error
+ ;
+
+ErrorList
+ : error
+ | Errors ',' error
+ ;
+
+JoinType_EDIT
+ : 'FULL' 'CURSOR' error
+   {
+     parser.suggestKeywords(['JOIN', 'OUTER JOIN']);
+   }
+ | 'LEFT' 'CURSOR' error
+   {
+     parser.suggestKeywords(['JOIN', 'OUTER JOIN']);
+   }
+ | 'RIGHT' 'CURSOR' error
+   {
+     parser.suggestKeywords(['JOIN', 'OUTER JOIN']);
+   }
+ ;
+
+OptionalSelectConditions_EDIT
+ : WhereClause error 'CURSOR' OptionalGroupByClause OptionalHavingClause OptionalOrderByClause OptionalLimitClause
+   {
+     $$ = {
+       suggestKeywords: parser.getKeywordsForOptionalsLR([$4, $5, $6, $7], [{ value: 'GROUP BY', weight: 8 }, { value: 'HAVING', weight: 7 }, { value: 'ORDER BY', weight: 5 }, { value: 'LIMIT', weight: 3 }], [true, true, true, true]),
+       cursorAtEnd: !$4 && !$5 && !$6 && !$7
+     };
+   }
+ | OptionalWhereClause OptionalGroupByClause HavingClause error 'CURSOR' OptionalOrderByClause OptionalLimitClause
+   {
+     $$ = {
+       suggestKeywords: parser.getKeywordsForOptionalsLR([$6, $7], [{ value: 'ORDER BY', weight: 5 }, { value: 'LIMIT', weight: 3 }], [true, true]),
+       cursorAtEnd: !$6 && !$7
+     }
+   }
+ | OptionalWhereClause OptionalGroupByClause OptionalHavingClause OrderByClause error 'CURSOR' OptionalLimitClause
+   {
+     $$ = {
+       suggestKeywords: parser.getKeywordsForOptionalsLR([$7], [{ value: 'LIMIT', weight: 3 }], [true]),
+       cursorAtEnd: !$7
+     }
+   }
+ | OptionalWhereClause OptionalGroupByClause OptionalHavingClause OptionalOrderByClause LimitClause error 'CURSOR'
+ ;
+
+OptionalSelectConditions_EDIT
+ : WhereClause error GroupByClause_EDIT OptionalHavingClause OptionalOrderByClause OptionalLimitClause
+ | WhereClause error OptionalGroupByClause HavingClause_EDIT OptionalOrderByClause OptionalLimitClause
+ | WhereClause error OptionalGroupByClause OptionalHavingClause OrderByClause_EDIT OptionalLimitClause
+ | WhereClause error OptionalGroupByClause OptionalHavingClause OptionalOrderByClause LimitClause_EDIT
+ | OptionalWhereClause GroupByClause error HavingClause_EDIT OptionalOrderByClause OptionalLimitClause
+ | OptionalWhereClause GroupByClause error OptionalHavingClause OrderByClause_EDIT OptionalLimitClause
+ | OptionalWhereClause GroupByClause error OptionalHavingClause OptionalOrderByClause LimitClause_EDIT
+ | OptionalWhereClause OptionalGroupByClause HavingClause error OrderByClause_EDIT OptionalLimitClause
+ | OptionalWhereClause OptionalGroupByClause HavingClause error OptionalOrderByClause LimitClause_EDIT
+ | OptionalWhereClause OptionalGroupByClause OptionalHavingClause OrderByClause error LimitClause_EDIT
+ ;
+
+DatabaseDefinition_EDIT
+ : 'CREATE' DatabaseOrSchema OptionalIfNotExists RegularIdentifier DatabaseDefinitionOptionals_EDIT error
+ ;

+ 72 - 0
desktop/core/src/desktop/js/parse/jison/sql/ksql/sql_insert.jison

@@ -0,0 +1,72 @@
+// 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.
+
+DataManipulation
+ : InsertStatement
+ ;
+
+InsertStatement
+ : InsertValuesStatement
+ ;
+
+DataManipulation_EDIT
+ : InsertValuesStatement_EDIT
+ ;
+
+InsertValuesStatement
+ : 'INSERT' 'INTO' OptionalTable SchemaQualifiedTableIdentifier 'VALUES' InsertValuesList
+   {
+     $4.owner = 'insert';
+     parser.addTablePrimary($4);
+   }
+ ;
+
+InsertValuesStatement_EDIT
+ : 'INSERT' 'CURSOR'
+   {
+     parser.suggestKeywords(['INTO']);
+   }
+ | 'INSERT' 'INTO' OptionalTable 'CURSOR'
+   {
+     if (!$3) {
+       parser.suggestKeywords(['TABLE']);
+     }
+     parser.suggestTables();
+     parser.suggestDatabases({ appendDot: true });
+   }
+ | 'INSERT' 'INTO' OptionalTable SchemaQualifiedTableIdentifier_EDIT
+ | 'INSERT' 'INTO' OptionalTable SchemaQualifiedTableIdentifier 'CURSOR'
+   {
+     $4.owner = 'insert';
+     parser.addTablePrimary($4);
+     parser.suggestKeywords(['VALUES']);
+   }
+ | 'INSERT' 'INTO' OptionalTable SchemaQualifiedTableIdentifier_EDIT 'VALUES' InsertValuesList
+ ;
+
+InsertValuesList
+ : ParenthesizedRowValuesList
+ | RowValuesList ',' ParenthesizedRowValuesList
+ ;
+
+ParenthesizedRowValuesList
+ : '(' InValueList ')'
+ ;
+
+OptionalTable
+ :
+ | 'TABLE'
+ ;

+ 2762 - 0
desktop/core/src/desktop/js/parse/jison/sql/ksql/sql_main.jison

@@ -0,0 +1,2762 @@
+// 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.
+
+SqlSyntax
+ : NewStatement SqlStatements EOF
+ ;
+
+SqlAutocomplete
+ : NewStatement SqlStatements EOF
+   {
+     return parser.yy.result;
+   }
+ | NewStatement SqlStatements_EDIT EOF
+   {
+     return parser.yy.result;
+   }
+ ;
+
+NewStatement
+ : /* empty */
+   {
+     parser.prepareNewStatement();
+   }
+ ;
+
+SqlStatements
+ :
+ | SqlStatement
+   {
+     parser.addStatementLocation(@1);
+   }
+ | SqlStatements ';' NewStatement SqlStatements
+ ;
+
+SqlStatements_EDIT
+ : SqlStatement_EDIT
+   {
+     parser.addStatementLocation(@1);
+   }
+ | SqlStatement_EDIT ';' NewStatement SqlStatements
+   {
+     parser.addStatementLocation(@1);
+   }
+ | SqlStatements ';' NewStatement SqlStatement_EDIT
+   {
+     parser.addStatementLocation(@4);
+   }
+ | SqlStatements ';' NewStatement SqlStatement_EDIT ';' NewStatement SqlStatements
+   {
+     parser.addStatementLocation(@4);
+   }
+ ;
+
+SqlStatement
+ : DataDefinition
+ | DataManipulation
+ | QuerySpecification
+ ;
+
+SqlStatement_EDIT
+ : AnyCursor
+   {
+     parser.suggestDdlAndDmlKeywords();
+   }
+ | CommonTableExpression 'CURSOR'
+   {
+     parser.suggestKeywords(['SELECT']);
+   }
+ | DataDefinition_EDIT
+ | DataManipulation_EDIT
+ | QuerySpecification_EDIT
+ | SetSpecification_EDIT
+ ;
+
+NonReservedKeyword
+ : 'ROLE'
+ | 'OPTION'
+ | 'STRUCT'
+ ;
+
+RegularIdentifier
+ : 'REGULAR_IDENTIFIER'
+ | 'VARIABLE_REFERENCE'
+ | NonReservedKeyword
+ ;
+
+// This is a work-around for error handling when a statement starts with some token that the parser can understand but
+// it's not a valid statement (see ErrorStatement). It contains everything except valid starting tokens ('SELECT', 'USE' etc.)
+NonStartingToken
+ : '!'
+ | '('
+ | ')'
+ | '*'
+ | ','
+ | '-'
+ | '.'
+ | '<'
+ | '='
+ | '>'
+ | '['
+ | ']'
+ | '~'
+ | 'ALL'
+ | 'ANALYTIC'
+ | 'AND'
+ | 'ARITHMETIC_OPERATOR'
+ | 'ARRAY'
+ | 'AS'
+ | 'ASC'
+ | 'AVG'
+ | 'BACKTICK'
+ | 'BETWEEN'
+ | 'BIGINT'
+ | 'BOOLEAN'
+ | 'BY'
+ | 'CASE'
+ | 'CAST'
+ | 'CHAR'
+ | 'COMPARISON_OPERATOR'
+ | 'COUNT'
+ | 'CROSS'
+ | 'CURRENT'
+ | 'DATABASE'
+ | 'DECIMAL'
+ | 'DESC'
+ | 'DISTINCT'
+ | 'DOUBLE'
+ | 'DOUBLE_QUOTE'
+ | 'ELSE'
+ | 'END'
+ | 'EXISTS'
+ | 'FALSE'
+ | 'FLOAT'
+ | 'FOLLOWING'
+ | 'FROM'
+ | 'FULL'
+ | 'GROUP'
+ | 'HAVING'
+ | 'HDFS_START_QUOTE'
+ | 'IF'
+ | 'IN'
+ | 'INNER'
+ | 'INT'
+ | 'INTO'
+ | 'IS'
+ | 'JOIN'
+ | 'LEFT'
+ | 'LIKE'
+ | 'LIMIT'
+ | 'MAP'
+ | 'MAX'
+ | 'MIN'
+ | 'NOT'
+ | 'NULL'
+ | 'ON'
+ | 'OPTION'
+ | 'OR'
+ | 'ORDER'
+ | 'OUTER'
+ | 'OVER'
+ | 'PARTITION'
+ | 'PRECEDING'
+ | 'PURGE'
+ | 'RANGE'
+ | 'REGEXP'
+ | 'REGULAR_IDENTIFIER'
+ | 'RIGHT'
+ | 'RLIKE'
+ | 'ROLE'
+ | 'ROW'
+ | 'ROWS'
+ | 'SCHEMA'
+ | 'SEMI'
+ | 'SET'
+ | 'SINGLE_QUOTE'
+ | 'SMALLINT'
+ | 'STDDEV_POP'
+ | 'STDDEV_SAMP'
+ | 'STRING'
+ | 'STRUCT'
+ | 'SUM'
+ | 'TABLE'
+ | 'THEN'
+ | 'TIMESTAMP'
+ | 'TINYINT'
+ | 'TRUE'
+ | 'UNION'
+ | 'UNSIGNED_INTEGER'
+ | 'UNSIGNED_INTEGER_E'
+ | 'VALUES'
+ | 'VAR_POP'
+ | 'VAR_SAMP'
+ | 'VARCHAR'
+ | 'VARIABLE_REFERENCE'
+ | 'VARIANCE'
+ | 'WHEN'
+ | 'WHERE'
+ ;
+
+// ===================================== Commonly used constructs =====================================
+
+Commas
+ : ','
+ | Commas ','
+ ;
+
+AnyCursor
+ : 'CURSOR'
+ | 'PARTIAL_CURSOR'
+ ;
+
+FromOrIn
+ : 'FROM'
+ | 'IN'
+ ;
+
+DatabaseOrSchema
+ : 'DATABASE'
+ | 'SCHEMA'
+ ;
+
+SingleQuotedValue
+ : 'SINGLE_QUOTE' 'VALUE' 'SINGLE_QUOTE'  -> $2
+ | 'SINGLE_QUOTE' 'SINGLE_QUOTE'          -> ''
+ ;
+
+SingleQuotedValue_EDIT
+ : 'SINGLE_QUOTE' 'PARTIAL_VALUE'
+ ;
+
+DoubleQuotedValue
+ : 'DOUBLE_QUOTE' 'VALUE' 'DOUBLE_QUOTE'  -> $2
+ | 'DOUBLE_QUOTE' 'DOUBLE_QUOTE'          -> ''
+ ;
+
+DoubleQuotedValue_EDIT
+ : 'DOUBLE_QUOTE' 'PARTIAL_VALUE'
+ ;
+
+QuotedValue
+ : SingleQuotedValue
+ | DoubleQuotedValue
+ ;
+
+QuotedValue_EDIT
+ : SingleQuotedValue_EDIT
+ | DoubleQuotedValue_EDIT
+ ;
+
+OptionalFromDatabase
+ :
+ | FromOrIn DatabaseIdentifier
+ ;
+
+OptionalFromDatabase_EDIT
+ : FromOrIn DatabaseIdentifier_EDIT
+ ;
+
+OptionalCascade
+ :
+ | 'CASCADE'
+ ;
+
+OptionalIfExists
+ :
+ | 'IF' 'EXISTS'
+   {
+     parser.yy.correlatedSubQuery = false;
+   }
+ ;
+
+OptionalIfExists_EDIT
+ : 'IF' 'CURSOR'
+   {
+     parser.suggestKeywords(['EXISTS']);
+   }
+ ;
+
+OptionalIfNotExists
+ :
+ | 'IF' 'NOT' 'EXISTS'
+   {
+     parser.yy.correlatedSubQuery = false;
+   }
+ ;
+
+OptionalIfNotExists_EDIT
+ : 'IF' 'CURSOR'
+   {
+     parser.suggestKeywords(['NOT EXISTS']);
+   }
+ | 'IF' 'NOT' 'CURSOR'
+   {
+     parser.suggestKeywords(['EXISTS']);
+   }
+ ;
+
+OptionalInDatabase
+ :
+ | 'IN' DatabaseIdentifier
+ | 'IN' DatabaseIdentifier_EDIT
+ ;
+
+OptionalPartitionSpec
+ :
+ | PartitionSpec
+ ;
+
+OptionalPartitionSpec_EDIT
+ : PartitionSpec_EDIT
+ ;
+
+PartitionSpec
+ : 'PARTITION' '(' PartitionSpecList ')'
+ ;
+
+PartitionSpec_EDIT
+ : 'PARTITION' '(' PartitionSpecList_EDIT RightParenthesisOrError
+ ;
+
+RangePartitionSpec
+ : UnsignedValueSpecification RangePartitionComparisonOperator 'VALUES' RangePartitionComparisonOperator UnsignedValueSpecification
+ ;
+
+RangePartitionSpec_EDIT
+ : UnsignedValueSpecification 'CURSOR'
+   {
+     parser.suggestKeywords(['<', '<=', '<>', '=', '>', '>=']);
+   }
+ | UnsignedValueSpecification RangePartitionComparisonOperator 'CURSOR'
+   {
+     parser.suggestKeywords(['VALUES']);
+   }
+ | UnsignedValueSpecification RangePartitionComparisonOperator 'VALUES' 'CURSOR'
+   {
+     parser.suggestKeywords(['<', '<=', '<>', '=', '>', '>=']);
+   }
+ | UnsignedValueSpecification 'CURSOR' 'VALUES' RangePartitionComparisonOperator UnsignedValueSpecification
+   {
+     parser.suggestKeywords(['<', '<=', '<>', '=', '>', '>=']);
+   }
+ | UnsignedValueSpecification RangePartitionComparisonOperator 'CURSOR' RangePartitionComparisonOperator UnsignedValueSpecification
+   {
+     parser.suggestKeywords(['VALUES']);
+   }
+ | UnsignedValueSpecification RangePartitionComparisonOperator 'VALUES' 'CURSOR' UnsignedValueSpecification
+   {
+     parser.suggestKeywords(['<', '<=', '<>', '=', '>', '>=']);
+   }
+ ;
+
+RangePartitionComparisonOperator
+ : 'COMPARISON_OPERATOR'
+ | '='
+ | '<'
+ | '>'
+ ;
+
+ConfigurationName
+ : RegularIdentifier
+ | 'CURSOR'
+ ;
+
+PartialBacktickedOrAnyCursor
+ : AnyCursor
+ | PartialBacktickedIdentifier
+ ;
+
+PartialBacktickedOrCursor
+ : 'CURSOR'
+ | PartialBacktickedIdentifier
+ ;
+
+PartialBacktickedOrPartialCursor
+ : 'PARTIAL_CURSOR'
+ | PartialBacktickedIdentifier
+ ;
+
+PartialBacktickedIdentifier
+ : 'BACKTICK' 'PARTIAL_VALUE'
+ ;
+
+RightParenthesisOrError
+ : ')'
+ | error
+ ;
+
+OptionalParenthesizedColumnList
+ :
+ | ParenthesizedColumnList
+ ;
+
+OptionalParenthesizedColumnList_EDIT
+ : ParenthesizedColumnList_EDIT
+ ;
+
+ParenthesizedColumnList
+ : '(' ColumnList ')'
+ ;
+
+ParenthesizedColumnList_EDIT
+ : '(' ColumnList_EDIT RightParenthesisOrError
+ | '(' AnyCursor RightParenthesisOrError
+   {
+     parser.suggestColumns();
+   }
+ ;
+
+ColumnList
+ : ColumnIdentifier
+ | ColumnList ',' ColumnIdentifier
+ ;
+
+ColumnList_EDIT
+ : ColumnList ',' AnyCursor
+   {
+     parser.suggestColumns();
+   }
+ | ColumnList ',' AnyCursor ',' ColumnList
+   {
+     parser.suggestColumns();
+   }
+ ;
+
+ParenthesizedSimpleValueList
+ : '(' SimpleValueList ')'
+ ;
+
+SimpleValueList
+ : UnsignedValueSpecification
+ | SimpleValueList ',' UnsignedValueSpecification
+ ;
+
+SchemaQualifiedTableIdentifier
+ : RegularOrBacktickedIdentifier
+   {
+     parser.addTableLocation(@1, [ { name: $1 } ]);
+     $$ = { identifierChain: [ { name: $1 } ] };
+   }
+ | RegularOrBacktickedIdentifier '.' RegularOrBacktickedIdentifier
+   {
+     parser.addDatabaseLocation(@1, [ { name: $1 } ]);
+     parser.addTableLocation(@3, [ { name: $1 }, { name: $3 } ]);
+     $$ = { identifierChain: [ { name: $1 }, { name: $3 } ] };
+   }
+ ;
+
+SchemaQualifiedTableIdentifier_EDIT
+ : PartialBacktickedIdentifier
+   {
+     parser.suggestTables();
+     parser.suggestDatabases({ appendDot: true });
+   }
+ | PartialBacktickedIdentifier '.' RegularOrBacktickedIdentifier
+   {
+     parser.suggestDatabases();
+     $$ = { identifierChain: [{ name: $1 }] };
+   }
+ | RegularOrBacktickedIdentifier '.' PartialBacktickedOrPartialCursor
+   {
+     parser.suggestTablesOrColumns($1);
+   }
+ ;
+
+SchemaQualifiedIdentifier
+ : RegularOrBacktickedIdentifier                                    -> [{ name: $1 }]
+ | RegularOrBacktickedIdentifier '.' RegularOrBacktickedIdentifier  -> [{ name: $1 }, { name: $2 }]
+ ;
+
+SchemaQualifiedIdentifier_EDIT
+ : PartialBacktickedIdentifier
+   {
+     parser.suggestDatabases({ appendDot: true });
+   }
+ | PartialBacktickedIdentifier '.' RegularOrBacktickedIdentifier
+   {
+     parser.suggestDatabases();
+     $$ = { identifierChain: [{ name: $1 }] };
+   }
+ | RegularOrBacktickedIdentifier '.' PartialBacktickedOrPartialCursor
+ ;
+
+DatabaseIdentifier
+ : RegularOrBacktickedIdentifier
+ ;
+
+DatabaseIdentifier_EDIT
+ : PartialBacktickedOrCursor
+   {
+     parser.suggestDatabases();
+   }
+ ;
+
+PartitionSpecList
+ : PartitionExpression
+ | PartitionSpecList ',' PartitionExpression
+ ;
+
+PartitionSpecList_EDIT
+ : PartitionExpression_EDIT
+ | PartitionSpecList ',' PartitionExpression_EDIT
+ | PartitionExpression_EDIT ',' PartitionSpecList
+ | PartitionSpecList ',' PartitionExpression_EDIT ',' PartitionSpecList
+ ;
+
+PartitionExpression
+ : ColumnIdentifier '=' ValueExpression
+ ;
+
+PartitionExpression_EDIT
+ : ColumnIdentifier '=' ValueExpression_EDIT
+ | ColumnIdentifier '=' AnyCursor
+   {
+     parser.valueExpressionSuggest();
+   }
+ | PartialBacktickedIdentifier '=' ValueExpression
+   {
+     parser.suggestColumns();
+   }
+ | AnyCursor
+   {
+     parser.suggestColumns();
+   }
+ ;
+
+RegularOrBacktickedIdentifier
+ : RegularIdentifier
+ | 'BACKTICK' 'VALUE' 'BACKTICK'  -> $2
+ | 'BACKTICK' 'BACKTICK'          -> ''
+ ;
+
+// TODO: Same as SchemaQualifiedTableIdentifier?
+RegularOrBackTickedSchemaQualifiedName
+ : RegularOrBacktickedIdentifier
+   {
+     parser.addTableLocation(@1, [ { name: $1 } ]);
+     $$ = { identifierChain: [ { name: $1 } ] };
+   }
+ | RegularOrBacktickedIdentifier '.' RegularOrBacktickedIdentifier
+   {
+     parser.addDatabaseLocation(@1, [ { name: $1 } ]);
+     parser.addTableLocation(@3, [ { name: $1 }, { name: $3 } ]);
+     $$ = { identifierChain: [ { name: $1 }, { name: $3 } ] };
+   }
+ ;
+
+RegularOrBackTickedSchemaQualifiedName_EDIT
+ : PartialBacktickedIdentifier
+   {
+     parser.suggestTables();
+     parser.suggestDatabases({ prependDot: true });
+   }
+ | RegularOrBacktickedIdentifier '.' PartialBacktickedOrPartialCursor
+   {
+     parser.suggestTablesOrColumns($1);
+   }
+ ;
+
+
+LocalOrSchemaQualifiedName
+ : RegularOrBackTickedSchemaQualifiedName
+ | RegularOrBackTickedSchemaQualifiedName RegularOrBacktickedIdentifier  -> { identifierChain: $1.identifierChain, alias: $2 }
+ ;
+
+LocalOrSchemaQualifiedName_EDIT
+ : RegularOrBackTickedSchemaQualifiedName_EDIT
+ | RegularOrBackTickedSchemaQualifiedName_EDIT RegularOrBacktickedIdentifier
+ ;
+
+ColumnReference
+ : BasicIdentifierChain
+   {
+     parser.yy.locations[parser.yy.locations.length - 1].type = 'column';
+   }
+ | BasicIdentifierChain '.' '*'
+   {
+     parser.addAsteriskLocation(@3, $1.concat({ asterisk: true }));
+   }
+ ;
+
+ColumnReference_EDIT
+ : BasicIdentifierChain_EDIT
+ ;
+
+BasicIdentifierChain
+ : ColumnIdentifier
+   {
+     $$ = [ $1.identifier ];
+     parser.yy.firstChainLocation = parser.addUnknownLocation($1.location, [ $1.identifier ]);
+   }
+ | BasicIdentifierChain '.' ColumnIdentifier
+   {
+     if (parser.yy.firstChainLocation) {
+       parser.yy.firstChainLocation.firstInChain = true;
+       delete parser.yy.firstChainLocation;
+     }
+     $1.push($3.identifier);
+     parser.addUnknownLocation($3.location, $1.concat());
+   }
+ ;
+
+// TODO: Merge with DerivedColumnChain_EDIT ( issue is starting with PartialBacktickedOrPartialCursor)
+BasicIdentifierChain_EDIT
+ : BasicIdentifierChain '.' PartialBacktickedOrPartialCursor
+   {
+     parser.suggestColumns({
+       identifierChain: $1
+     });
+     $$ = { suggestKeywords: [{ value: '*', weight: 10000 }] };
+   }
+ | BasicIdentifierChain '.' PartialBacktickedOrPartialCursor '.' BasicIdentifierChain
+   {
+     parser.suggestColumns({
+       identifierChain: $1
+     });
+     $$ = { suggestKeywords: [{ value: '*', weight: 10000 }] };
+   }
+ ;
+
+DerivedColumnChain
+ : ColumnIdentifier  -> [ $1.identifier ]
+ | DerivedColumnChain '.' ColumnIdentifier
+   {
+     $1.push($3.identifier);
+   }
+ ;
+
+DerivedColumnChain_EDIT
+ : PartialBacktickedIdentifierOrPartialCursor
+   {
+     parser.suggestColumns();
+   }
+ | DerivedColumnChain '.' PartialBacktickedIdentifierOrPartialCursor
+   {
+     parser.suggestColumns({ identifierChain: $1 });
+   }
+ | DerivedColumnChain '.' PartialBacktickedIdentifierOrPartialCursor '.' DerivedColumnChain
+   {
+     parser.suggestColumns({ identifierChain: $1 });
+   }
+ | PartialBacktickedIdentifierOrPartialCursor '.' DerivedColumnChain
+   {
+     parser.suggestColumns();
+   }
+ ;
+
+ColumnIdentifier
+ : RegularOrBacktickedIdentifier -> { identifier: { name: $1 }, location: @1 }
+ ;
+
+PartialBacktickedIdentifierOrPartialCursor
+ : PartialBacktickedIdentifier
+ | 'PARTIAL_CURSOR'
+ ;
+
+PrimitiveType
+ : 'BIGINT'
+ | 'BOOLEAN'
+ | 'CHAR' OptionalTypeLength
+ | 'DECIMAL' OptionalTypePrecision
+ | 'DOUBLE'
+ | 'FLOAT'
+ | 'INT'
+ | 'SMALLINT'
+ | 'STRING'
+ | 'TIMESTAMP'
+ | 'TINYINT'
+ | 'VARCHAR' OptionalTypeLength
+ ;
+
+OptionalTypeLength
+ :
+ | '(' 'UNSIGNED_INTEGER' ')'
+ ;
+
+OptionalTypePrecision
+ :
+ | '(' 'UNSIGNED_INTEGER' ')'
+ | '(' 'UNSIGNED_INTEGER' ',' 'UNSIGNED_INTEGER' ')'
+ ;
+
+// ===================================== SELECT statement =====================================
+
+QuerySpecification
+ : SelectStatement OptionalUnions                                   -> $1
+ | CommonTableExpression SelectStatement OptionalUnions
+ | CommonTableExpression '(' QuerySpecification ')' OptionalUnions  -> $3
+ ;
+
+QuerySpecification_EDIT
+ : SelectStatement_EDIT OptionalUnions
+ | SelectStatement OptionalUnions_EDIT
+ | CommonTableExpression '(' QuerySpecification_EDIT ')'
+   {
+     parser.addCommonTableExpressions($1);
+   }
+ | CommonTableExpression SelectStatement_EDIT OptionalUnions
+   {
+     parser.addCommonTableExpressions($1);
+   }
+ | CommonTableExpression SelectStatement OptionalUnions_EDIT
+   {
+     parser.addCommonTableExpressions($1);
+   }
+ | CommonTableExpression_EDIT
+ | CommonTableExpression_EDIT '(' QuerySpecification ')'
+ | CommonTableExpression_EDIT SelectStatement OptionalUnions
+ ;
+
+SelectStatement
+ : 'SELECT' OptionalAllOrDistinct SelectList
+   {
+     parser.addClauseLocation('selectList', parser.firstDefined($2, @2, $1, @1), @3);
+     $$ = { selectList: $3 };
+   }
+ | 'SELECT' OptionalAllOrDistinct SelectList TableExpression
+   {
+     parser.addClauseLocation('selectList', parser.firstDefined($2, @2, $1, @1), @3);
+     $$ = { selectList: $3, tableExpression: $4 }
+   }
+ ;
+
+OptionalUnions
+ :
+ | Unions
+ ;
+
+OptionalUnions_EDIT
+ : Unions_EDIT
+ ;
+
+Unions
+ : UnionClause
+ | Unions UnionClause
+ ;
+
+Unions_EDIT
+ : UnionClause_EDIT
+ | Unions UnionClause_EDIT
+ | UnionClause_EDIT Unions
+ | Unions UnionClause_EDIT Unions
+ ;
+
+UnionClause
+ : 'UNION' NewStatement OptionalAllOrDistinct SelectStatement
+ ;
+
+UnionClause_EDIT
+ : 'UNION' NewStatement 'CURSOR'
+   {
+     parser.suggestKeywords(['ALL', 'DISTINCT', 'SELECT']);
+   }
+ | 'UNION' NewStatement 'CURSOR' SelectStatement
+   {
+     parser.suggestKeywords(['ALL', 'DISTINCT']);
+   }
+ | 'UNION' NewStatement OptionalAllOrDistinct SelectStatement_EDIT
+ ;
+
+SelectStatement_EDIT
+ : 'SELECT' OptionalAllOrDistinct SelectList_EDIT
+   {
+     parser.addClauseLocation('selectList', parser.firstDefined($2, @2, $1, @1), @3);
+     if ($3.cursorAtStart) {
+       var keywords = parser.getSelectListKeywords();
+       if (!$2) {
+         keywords.push({ value: 'ALL', weight: 2 });
+         keywords.push({ value: 'DISTINCT', weight: 2 });
+       }
+       parser.suggestKeywords(keywords);
+     } else {
+       parser.checkForSelectListKeywords($4);
+     }
+     if ($3.suggestFunctions) {
+       parser.suggestFunctions();
+     }
+     if ($3.suggestColumns) {
+       parser.suggestColumns({ identifierChain: [], source: 'select' });
+     }
+     if ($3.suggestTables) {
+       parser.suggestTables({ prependQuestionMark: true, prependFrom: true });
+     }
+     if ($3.suggestDatabases) {
+       parser.suggestDatabases({ prependQuestionMark: true, prependFrom: true, appendDot: true });
+     }
+     if ($3.suggestAggregateFunctions && (!$2 || $2 === 'ALL')) {
+       parser.suggestAggregateFunctions();
+       parser.suggestAnalyticFunctions();
+     }
+   }
+ | 'SELECT' OptionalAllOrDistinct 'CURSOR'
+   {
+     parser.addClauseLocation('selectList', parser.firstDefined($2, @2, $1, @1), @3, true);
+     var keywords = parser.getSelectListKeywords();
+     if (!$2 || $2 === 'ALL') {
+       parser.suggestAggregateFunctions();
+       parser.suggestAnalyticFunctions();
+     }
+     if (!$2) {
+       keywords.push({ value: 'ALL', weight: 2 });
+       keywords.push({ value: 'DISTINCT', weight: 2 });
+     }
+     parser.suggestKeywords(keywords);
+     parser.suggestFunctions();
+     parser.suggestColumns({ identifierChain: [], source: 'select' });
+     parser.suggestTables({ prependQuestionMark: true, prependFrom: true });
+     parser.suggestDatabases({ prependQuestionMark: true, prependFrom: true, appendDot: true });
+   }
+ | 'SELECT' OptionalAllOrDistinct SelectList TableExpression_EDIT
+   {
+     parser.addClauseLocation('selectList', parser.firstDefined($2, @2, $1, @1), @3);
+   }
+ | 'SELECT' OptionalAllOrDistinct SelectList_EDIT TableExpression
+   {
+     parser.addClauseLocation('selectList', parser.firstDefined($2, @2, $1, @1), @3);
+     parser.selectListNoTableSuggest($3, $2);
+     if (parser.yy.result.suggestColumns) {
+       parser.yy.result.suggestColumns.source = 'select';
+     }
+   }
+ | 'SELECT' OptionalAllOrDistinct 'CURSOR' TableExpression
+   {
+     parser.addClauseLocation('selectList', parser.firstDefined($2, @2, $1, @1), @3, true);
+     var keywords = parser.getSelectListKeywords();
+     if (!$2 || $2 === 'ALL') {
+       parser.suggestAggregateFunctions();
+       parser.suggestAnalyticFunctions();
+     }
+     if (!$2) {
+       keywords.push({ value: 'ALL', weight: 2 });
+       keywords.push({ value: 'DISTINCT', weight: 2 });
+     }
+     parser.suggestKeywords(keywords);
+     parser.suggestFunctions();
+     parser.suggestColumns({ identifierChain: [], source: 'select' });
+     parser.suggestTables({ prependQuestionMark: true, prependFrom: true });
+     parser.suggestDatabases({ prependQuestionMark: true, prependFrom: true, appendDot: true });
+   }
+ | 'SELECT' OptionalAllOrDistinct SelectList 'CURSOR' TableExpression
+   {
+     parser.addClauseLocation('selectList', parser.firstDefined($2, @2, $1, @1), @3);
+     parser.checkForSelectListKeywords($3);
+   }
+ | 'SELECT' OptionalAllOrDistinct SelectList 'CURSOR' ',' TableExpression
+   {
+     parser.addClauseLocation('selectList', parser.firstDefined($2, @2, $1, @1), @3);
+     parser.checkForSelectListKeywords($3);
+   }
+ | 'SELECT' OptionalAllOrDistinct SelectList 'CURSOR'
+   {
+     parser.addClauseLocation('selectList', parser.firstDefined($2, @2, $1, @1), @3);
+     parser.checkForSelectListKeywords($3);
+     var keywords = ['FROM'];
+     if (parser.yy.result.suggestKeywords) {
+       keywords = parser.yy.result.suggestKeywords.concat(keywords);
+     }
+     parser.suggestKeywords(keywords);
+     parser.suggestTables({ prependFrom: true });
+     parser.suggestDatabases({ prependFrom: true, appendDot: true });
+   }
+ ;
+
+CommonTableExpression
+ : 'WITH' WithQueries  -> $2
+ ;
+
+CommonTableExpression_EDIT
+ : 'WITH' WithQueries_EDIT
+ ;
+
+WithQueries
+ : WithQuery                   -> [$1]
+ | WithQueries ',' WithQuery   -> $1.concat([$3])
+ ;
+
+WithQueries_EDIT
+ : WithQuery_EDIT
+ | WithQueries ',' WithQuery_EDIT
+   {
+     parser.addCommonTableExpressions($1);
+   }
+ | WithQuery_EDIT ',' WithQueries
+ | WithQueries ',' WithQuery_EDIT ',' WithQueries
+   {
+     parser.addCommonTableExpressions($1);
+   }
+ ;
+
+WithQuery
+ : RegularOrBacktickedIdentifier 'AS' '(' TableSubQueryInner ')'
+   {
+     parser.addCteAliasLocation(@1, $1);
+     $4.alias = $1;
+     $$ = $4;
+   }
+ ;
+
+WithQuery_EDIT
+ : RegularOrBacktickedIdentifier 'CURSOR'
+   {
+     parser.suggestKeywords(['AS']);
+   }
+ | RegularOrBacktickedIdentifier 'AS' '(' AnyCursor RightParenthesisOrError
+   {
+     parser.suggestKeywords(['SELECT']);
+   }
+ | RegularOrBacktickedIdentifier 'AS' '(' TableSubQueryInner_EDIT RightParenthesisOrError
+ ;
+
+OptionalAllOrDistinct
+ :
+ | 'ALL'
+ | 'DISTINCT'
+ ;
+
+TableExpression
+ : FromClause OptionalSelectConditions
+   {
+     parser.addClauseLocation('whereClause', @1, $2.whereClauseLocation);
+     parser.addClauseLocation('limitClause', $2.limitClausePreceding || @1, $2.limitClauseLocation);
+   }
+ ;
+
+TableExpression_EDIT
+ : FromClause_EDIT OptionalSelectConditions
+   {
+     parser.addClauseLocation('whereClause', @1, $2.whereClauseLocation);
+     parser.addClauseLocation('limitClause', $2.limitClausePreceding || @1, $2.limitClauseLocation);
+   }
+ | FromClause 'CURSOR' OptionalSelectConditions OptionalJoins
+   {
+     var keywords = [];
+
+     parser.addClauseLocation('whereClause', @1, $3.whereClauseLocation);
+     parser.addClauseLocation('limitClause', $2.limitClausePreceding || @1, $2.limitClauseLocation);
+
+     if ($1) {
+       if (typeof $1.tableReferenceList.hasJoinCondition !== 'undefined' && !$1.tableReferenceList.hasJoinCondition) {
+         keywords.push({ value: 'ON', weight: 3 });
+       }
+       if ($1.suggestKeywords) {
+         keywords = parser.createWeightedKeywords($1.suggestKeywords, 3);
+       }
+       if ($1.tableReferenceList.suggestJoinConditions) {
+         parser.suggestJoinConditions($1.tableReferenceList.suggestJoinConditions);
+       }
+       if ($1.tableReferenceList.suggestJoins) {
+         parser.suggestJoins($1.tableReferenceList.suggestJoins);
+       }
+       if ($1.tableReferenceList.suggestKeywords) {
+         keywords = keywords.concat(parser.createWeightedKeywords($1.tableReferenceList.suggestKeywords, 3));
+       }
+
+       // Lower the weights for 'TABLESAMPLE'
+       keywords.forEach(function (keyword) {
+         if (keyword.value === 'TABLESAMPLE') {
+           keyword.weight = 1.1;
+         }
+       });
+
+       if ($1.tableReferenceList.types) {
+         var veKeywords = parser.getValueExpressionKeywords($1.tableReferenceList);
+         keywords = keywords.concat(veKeywords.suggestKeywords);
+         if (veKeywords.suggestColRefKeywords) {
+           parser.suggestColRefKeywords(veKeywords.suggestColRefKeywords);
+           parser.addColRefIfExists($1.tableReferenceList);
+         }
+       }
+     }
+
+     if ($3.empty && $4 && $4.joinType.toUpperCase() === 'JOIN') {
+       keywords = keywords.concat(['FULL', 'FULL OUTER', 'INNER', 'LEFT', 'LEFT OUTER', 'RIGHT', 'RIGHT OUTER']);
+       parser.suggestKeywords(keywords);
+       return;
+     }
+
+     if ($3.suggestKeywords) {
+       keywords = keywords.concat(parser.createWeightedKeywords($3.suggestKeywords, 2));
+     }
+
+     if ($3.suggestFilters) {
+       parser.suggestFilters($3.suggestFilters);
+     }
+     if ($3.suggestGroupBys) {
+       parser.suggestGroupBys($3.suggestGroupBys);
+     }
+     if ($3.suggestOrderBys) {
+       parser.suggestOrderBys($3.suggestOrderBys);
+     }
+
+     if ($3.empty) {
+       keywords.push({ value: 'UNION', weight: 2.11 });
+     }
+
+     keywords = keywords.concat([
+       { value: 'FULL JOIN', weight: 1 },
+       { value: 'FULL OUTER JOIN', weight: 1 },
+       { value: 'INNER JOIN', weight: 1 },
+       { value: 'JOIN', weight: 1 },
+       { value: 'LEFT JOIN', weight: 1 },
+       { value: 'LEFT OUTER JOIN', weight: 1 },
+       { value: 'RIGHT JOIN', weight: 1 },
+       { value: 'RIGHT OUTER JOIN', weight: 1 }
+     ]);
+     parser.suggestKeywords(keywords);
+  }
+ | FromClause OptionalSelectConditions_EDIT OptionalJoins
+   {
+     // A couple of things are going on here:
+     // - If there are no SelectConditions (WHERE, GROUP BY, etc.) we should suggest complete join options
+     // - If there's an OptionalJoin at the end, i.e. 'SELECT * FROM foo | JOIN ...' we should suggest
+     //   different join types
+     // - The FromClause could end with a valueExpression, in which case we should suggest keywords like '='
+     //   or 'AND' based on type
+
+     if (!$2) {
+       parser.addClauseLocation('whereClause', @1);
+       parser.addClauseLocation('limitClause', @1);
+       return;
+     }
+     parser.addClauseLocation('whereClause', @1, $2.whereClauseLocation);
+     parser.addClauseLocation('limitClause', $2.limitClausePreceding || @1, $2.limitClauseLocation);
+     var keywords = [];
+
+     if ($2.suggestColRefKeywords) {
+       parser.suggestColRefKeywords($2.suggestColRefKeywords);
+       parser.addColRefIfExists($2);
+     }
+
+     if ($2.suggestKeywords && $2.suggestKeywords.length) {
+       keywords = keywords.concat(parser.createWeightedKeywords($2.suggestKeywords, 2));
+     }
+
+     if ($2.cursorAtEnd) {
+       keywords.push({ value: 'UNION', weight: 2.11 });
+     }
+     parser.suggestKeywords(keywords);
+   }
+ ;
+
+OptionalJoins
+ :
+ | Joins
+ | Joins_INVALID
+ ;
+
+FromClause
+ : 'FROM' TableReferenceList
+   {
+     $$ = { tableReferenceList : $2 }
+   }
+ ;
+
+FromClause_EDIT
+ : 'FROM' 'CURSOR'
+   {
+       parser.suggestTables();
+       parser.suggestDatabases({ appendDot: true });
+   }
+ | 'FROM' TableReferenceList_EDIT
+ ;
+
+OptionalSelectConditions
+ : OptionalWhereClause OptionalGroupByClause OptionalHavingClause OptionalOrderByClause OptionalLimitClause
+   {
+     var keywords = parser.getKeywordsForOptionalsLR(
+       [$1, $2, $3, $4, $5],
+       [{ value: 'WHERE', weight: 7 }, { value: 'GROUP BY', weight: 6 }, { value: 'HAVING', weight: 5 }, { value: 'ORDER BY', weight: 4 }, { value: 'LIMIT', weight: 3 }],
+       [true, true, true, true, true]);
+
+     if (keywords.length > 0) {
+       $$ = { suggestKeywords: keywords, empty: !$1 && !$2 && !$3 && !$4 && !$5 };
+     } else {
+       $$ = {};
+     }
+
+     $$.whereClauseLocation = $1 ? @1 : undefined;
+     $$.limitClausePreceding = parser.firstDefined($4, @4, $3, @3, $2, @2, $1, @1);
+     $$.limitClauseLocation = $5 ? @5 : undefined;
+
+     if (!$1 && !$2 && !$3 && !$4 && !$5) {
+       $$.suggestFilters = { prefix: 'WHERE', tablePrimaries: parser.yy.latestTablePrimaries.concat() };
+     }
+     if (!$2 && !$3 && !$4 && !$5) {
+       $$.suggestGroupBys = { prefix: 'GROUP BY', tablePrimaries: parser.yy.latestTablePrimaries.concat() };
+     }
+     if (!$4 && !$5) {
+       $$.suggestOrderBys = { prefix: 'ORDER BY', tablePrimaries: parser.yy.latestTablePrimaries.concat() };
+     }
+   }
+ ;
+
+OptionalSelectConditions_EDIT
+ : WhereClause_EDIT OptionalGroupByClause OptionalHavingClause OptionalOrderByClause OptionalLimitClause
+   {
+     if (parser.yy.result.suggestColumns) {
+       parser.yy.result.suggestColumns.source = 'where';
+     }
+   }
+ | OptionalWhereClause GroupByClause_EDIT OptionalHavingClause OptionalOrderByClause OptionalLimitClause
+   {
+     if (parser.yy.result.suggestColumns) {
+       parser.yy.result.suggestColumns.source = 'group by';
+     }
+   }
+ | OptionalWhereClause OptionalGroupByClause HavingClause_EDIT OptionalOrderByClause OptionalLimitClause
+ | OptionalWhereClause OptionalGroupByClause OptionalHavingClause OrderByClause_EDIT OptionalLimitClause
+   {
+     if (parser.yy.result.suggestColumns) {
+       parser.yy.result.suggestColumns.source = 'order by';
+     }
+   }
+ | OptionalWhereClause OptionalGroupByClause OptionalHavingClause OptionalOrderByClause LimitClause_EDIT
+ ;
+
+OptionalSelectConditions_EDIT
+ : WhereClause 'CURSOR' OptionalGroupByClause OptionalHavingClause OptionalOrderByClause OptionalLimitClause
+   {
+     var keywords = parser.getKeywordsForOptionalsLR(
+       [$3, $4, $5, $6],
+       [{ value: 'GROUP BY', weight: 8 }, { value: 'HAVING', weight: 7 }, { value: 'ORDER BY', weight: 5 }, { value: 'LIMIT', weight: 3 }],
+       [true, true, true, true]);
+     if ($1.suggestKeywords) {
+       keywords = keywords.concat(parser.createWeightedKeywords($1.suggestKeywords, 1));
+     }
+     $$ = parser.getValueExpressionKeywords($1, keywords);
+     $$.cursorAtEnd = !$3 && !$4 && !$5 && !$6;
+     if ($1.columnReference) {
+       $$.columnReference = $1.columnReference;
+     }
+     if (!$3) {
+       parser.suggestGroupBys({ prefix: 'GROUP BY', tablePrimaries: parser.yy.latestTablePrimaries.concat() });
+     }
+     if (!$3 && !$4 && !$5) {
+       parser.suggestOrderBys({ prefix: 'ORDER BY', tablePrimaries: parser.yy.latestTablePrimaries.concat() });
+     }
+     $$.whereClauseLocation = $1 ? @1 : undefined;
+     $$.limitClausePreceding = parser.firstDefined($5, @5, $4, @4, $3, @3, $1, @1);
+     $$.limitClauseLocation = $6 ? @6 : undefined;
+   }
+ | OptionalWhereClause GroupByClause 'CURSOR' OptionalHavingClause OptionalOrderByClause OptionalLimitClause
+   {
+     var keywords = parser.getKeywordsForOptionalsLR(
+       [$4, $5, $6],
+       [{ value: 'HAVING', weight: 7 }, { value: 'ORDER BY', weight: 5 }, { value: 'LIMIT', weight: 3 }],
+       [true, true, true]);
+     if ($2.suggestKeywords) {
+       keywords = keywords.concat(parser.createWeightedKeywords($2.suggestKeywords, 8));
+     }
+     if ($2.valueExpression) {
+       $$ = parser.getValueExpressionKeywords($2.valueExpression, keywords);
+       if ($2.valueExpression.columnReference) {
+         $$.columnReference = $2.valueExpression.columnReference;
+       }
+     } else {
+       $$ = { suggestKeywords: keywords };
+     }
+     $$.cursorAtEnd = !$4 && !$5 && !$6;
+     if (!$4 && !$5) {
+       parser.suggestOrderBys({ prefix: 'ORDER BY', tablePrimaries: parser.yy.latestTablePrimaries.concat() });
+     }
+     $$.whereClauseLocation = $1 ? @1 : undefined;
+     $$.limitClausePreceding = parser.firstDefined($5, @5, $4, @4, $2, @2);
+     $$.limitClauseLocation = $6 ? @6 : undefined;
+   }
+ | OptionalWhereClause OptionalGroupByClause HavingClause 'CURSOR' OptionalOrderByClause OptionalLimitClause
+   {
+     var keywords = parser.getKeywordsForOptionalsLR(
+       [$5, $6],
+       [{ value: 'ORDER BY', weight: 5 }, { value: 'LIMIT', weight: 3 }],
+       [true, true]);
+     $$ = { suggestKeywords: keywords, cursorAtEnd: !$5 && !$6 };
+     if (!$5) {
+       parser.suggestOrderBys({ prefix: 'ORDER BY', tablePrimaries: parser.yy.latestTablePrimaries.concat() });
+     }
+     $$.whereClauseLocation = $1 ? @1 : undefined;
+     $$.limitClausePreceding = parser.firstDefined($5, @5, $3, @3);
+     $$.limitClauseLocation = $6 ? @6 : undefined;
+   }
+ | OptionalWhereClause OptionalGroupByClause OptionalHavingClause OrderByClause 'CURSOR' OptionalLimitClause
+   {
+     var keywords = parser.getKeywordsForOptionalsLR(
+       [$6],
+       [{ value: 'LIMIT', weight: 3 }],
+       [true]);
+     if ($4.suggestKeywords) {
+       keywords = keywords.concat(parser.createWeightedKeywords($4.suggestKeywords, 4));
+     }
+     $$ = { suggestKeywords: keywords, cursorAtEnd: !$6 };
+     $$.whereClauseLocation = $1 ? @1 : undefined;
+     $$.limitClausePreceding = parser.firstDefined($4, @4);
+     $$.limitClauseLocation = $6 ? @6 : undefined;
+   }
+ | OptionalWhereClause OptionalGroupByClause OptionalHavingClause OptionalOrderByClause LimitClause 'CURSOR'
+   {
+     $$ = { suggestKeywords: [], cursorAtEnd: true };
+     $$.whereClauseLocation = $1 ? @1 : undefined;
+     $$.limitClausePreceding = parser.firstDefined($4, @4, $3, @3, $2, @2, $1, @1);
+     $$.limitClauseLocation = @5;
+   }
+ ;
+
+OptionalWhereClause
+ :
+ | WhereClause
+ ;
+
+WhereClause
+ : 'WHERE' SearchCondition  -> $2
+ ;
+
+WhereClause_EDIT
+ : 'WHERE' SearchCondition_EDIT
+   {
+     if ($2.suggestFilters) {
+       parser.suggestFilters({ tablePrimaries: parser.yy.latestTablePrimaries.concat() });
+     }
+   }
+ | 'WHERE' 'CURSOR'
+   {
+     parser.suggestFunctions();
+     parser.suggestColumns();
+     parser.suggestKeywords(['EXISTS', 'NOT EXISTS']);
+     parser.suggestFilters({ tablePrimaries: parser.yy.latestTablePrimaries.concat() });
+   }
+ ;
+
+OptionalGroupByClause
+ :
+ | GroupByClause
+ ;
+
+GroupByClause
+ : 'GROUP' 'BY' GroupByColumnList
+   {
+     $$ = { valueExpression: $3 };
+   }
+ ;
+
+GroupByClause_EDIT
+ : 'GROUP' 'BY' GroupByColumnList_EDIT
+   {
+     parser.suggestSelectListAliases();
+   }
+ | 'GROUP' 'BY' 'CURSOR'
+   {
+     parser.valueExpressionSuggest();
+     parser.suggestSelectListAliases();
+     parser.suggestGroupBys({ tablePrimaries: parser.yy.latestTablePrimaries.concat() });
+   }
+ | 'GROUP' 'CURSOR'
+   {
+     parser.suggestKeywords(['BY']);
+     parser.suggestGroupBys({ prefix: 'BY', tablePrimaries: parser.yy.latestTablePrimaries.concat() });
+   }
+ ;
+
+ColumnGroupingSets
+ :
+ | ColumnReference
+ | ColumnGroupingSets ',' ColumnGroupingSets
+ | '(' ColumnGroupingSets ')'
+ ;
+
+ColumnGroupingSets_EDIT
+ : ColumnGroupingSet_EDIT
+ | ColumnGroupingSet_EDIT ',' ColumnGroupingSets
+ | ColumnGroupingSets ',' ColumnGroupingSet_EDIT
+ | ColumnGroupingSets ',' ColumnGroupingSet_EDIT ',' ColumnGroupingSets
+ | '(' ColumnGroupingSets_EDIT RightParenthesisOrError
+ ;
+
+ColumnGroupingSet_EDIT
+ : AnyCursor
+   {
+     parser.suggestColumns();
+   }
+ | ColumnReference_EDIT
+ ;
+
+GroupByColumnList
+ : ValueExpression
+ | GroupByColumnList ',' ValueExpression  -> $3
+ ;
+
+GroupByColumnList_EDIT
+ : ValueExpression_EDIT
+ | 'CURSOR' ValueExpression
+   {
+     parser.valueExpressionSuggest();
+   }
+ | 'CURSOR' ',' GroupByColumnList
+   {
+     parser.valueExpressionSuggest();
+   }
+ | ValueExpression_EDIT ',' GroupByColumnList
+ | GroupByColumnList ',' GroupByColumnListPartTwo_EDIT
+ | GroupByColumnList ',' GroupByColumnListPartTwo_EDIT ','
+ | GroupByColumnList ',' GroupByColumnListPartTwo_EDIT ',' GroupByColumnList
+ ;
+
+GroupByColumnListPartTwo_EDIT
+ : ValueExpression_EDIT
+ | AnyCursor ValueExpression
+   {
+     parser.valueExpressionSuggest();
+   }
+ | AnyCursor
+   {
+     parser.valueExpressionSuggest();
+   }
+ ;
+
+OptionalOrderByClause
+ :
+ | OrderByClause
+ ;
+
+OrderByClause
+ : 'ORDER' 'BY' OrderByColumnList  -> $3
+ ;
+
+OrderByClause_EDIT
+ : 'ORDER' 'BY' OrderByColumnList_EDIT
+   {
+     if ($3.emptyOrderBy) {
+       parser.suggestOrderBys({ tablePrimaries: parser.yy.latestTablePrimaries.concat() });
+     }
+   }
+ | 'ORDER' 'CURSOR'
+   {
+     parser.suggestKeywords(['BY']);
+     parser.suggestOrderBys({ prefix: 'BY', tablePrimaries: parser.yy.latestTablePrimaries.concat() });
+   }
+ ;
+
+OrderByColumnList
+ : OrderByIdentifier
+ | OrderByColumnList ',' OrderByIdentifier  -> $3
+ ;
+
+OrderByColumnList_EDIT
+ : OrderByIdentifier_EDIT
+ | 'CURSOR' OrderByIdentifier
+   {
+     $$ = { emptyOrderBy: false }
+     parser.valueExpressionSuggest();
+     parser.suggestAnalyticFunctions();
+     parser.suggestSelectListAliases();
+   }
+ | OrderByColumnList ',' OrderByIdentifier_EDIT                        -> { emptyOrderBy: false }
+ | OrderByColumnList ',' OrderByIdentifier_EDIT ','                    -> { emptyOrderBy: false }
+ | OrderByColumnList ',' OrderByIdentifier_EDIT ',' OrderByColumnList  -> { emptyOrderBy: false }
+ ;
+
+OrderByIdentifier
+ : ValueExpression OptionalAscOrDesc  -> parser.mergeSuggestKeywords($2)
+ ;
+
+OrderByIdentifier_EDIT
+ : ValueExpression_EDIT OptionalAscOrDesc
+   {
+     parser.suggestSelectListAliases();
+   }
+ | AnyCursor OptionalAscOrDesc
+   {
+     $$ = { emptyOrderBy: true }
+     parser.valueExpressionSuggest();
+     parser.suggestAnalyticFunctions();
+     parser.suggestSelectListAliases();
+   }
+ ;
+
+OptionalAscOrDesc
+ :
+  {
+    $$ = { suggestKeywords: ['ASC', 'DESC'] };
+  }
+ | 'ASC'
+ | 'DESC'
+ ;
+
+OptionalLimitClause
+ :
+ | LimitClause
+ ;
+
+LimitClause
+ : 'LIMIT' UnsignedNumericLiteral
+ | 'LIMIT' UnsignedNumericLiteral ',' UnsignedNumericLiteral
+ | 'LIMIT' 'VARIABLE_REFERENCE'
+ | 'LIMIT' 'VARIABLE_REFERENCE' ',' 'VARIABLE_REFERENCE'
+ ;
+
+LimitClause_EDIT
+ : 'LIMIT' 'CURSOR'
+ ;
+
+SearchCondition
+ : ValueExpression
+ ;
+
+SearchCondition_EDIT
+ : ValueExpression_EDIT
+ ;
+
+ValueExpression
+ : NonParenthesizedValueExpressionPrimary
+ ;
+
+ValueExpression_EDIT
+ : NonParenthesizedValueExpressionPrimary_EDIT
+ ;
+
+ValueExpression_EDIT
+ : ValueExpression 'NOT' 'CURSOR'
+   {
+     parser.suggestKeywords(['BETWEEN', 'EXISTS', 'IN', 'LIKE', 'REGEXP', 'RLIKE']);
+     $$ = { types: [ 'BOOLEAN' ] };
+   }
+ ;
+
+ValueExpressionList
+ : ValueExpression
+   {
+     $1.position = 1;
+   }
+ | ValueExpressionList ',' ValueExpression
+   {
+     $3.position = $1.position + 1;
+     $$ = $3;
+   }
+ ;
+
+ValueExpressionList_EDIT
+ : ValueExpression_EDIT
+   {
+     $1.position = 1;
+   }
+ | ValueExpressionList ',' ValueExpression_EDIT
+   {
+     $1.position += 1;
+   }
+ | ValueExpression_EDIT ',' ValueExpressionList
+   {
+     $1.position = 1;
+   }
+ | ValueExpressionList ',' ValueExpression_EDIT ',' ValueExpressionList
+   {
+     $1.position += 1;
+   }
+ | ValueExpressionList ',' AnyCursor
+   {
+     parser.valueExpressionSuggest();
+     $1.position += 1;
+   }
+ | ValueExpressionList ',' AnyCursor ',' ValueExpressionList
+   {
+     parser.valueExpressionSuggest();
+     $1.position += 1;
+   }
+ | ValueExpressionList 'CURSOR' ',' ValueExpressionList
+   {
+     parser.suggestValueExpressionKeywords($1);
+   }
+ | AnyCursor ',' ValueExpressionList
+   {
+     parser.valueExpressionSuggest();
+     $$ = { cursorAtStart : true, position: 1 };
+   }
+ | AnyCursor ','
+   {
+     parser.valueExpressionSuggest();
+     $$ = { cursorAtStart : true, position: 1 };
+   }
+ | ',' AnyCursor
+   {
+     parser.valueExpressionSuggest();
+     $$ = { position: 2 };
+   }
+ | ',' AnyCursor ',' ValueExpressionList
+   {
+     parser.valueExpressionSuggest();
+     $$ = { position: 2 };
+   }
+ ;
+
+InValueList
+ : NonParenthesizedValueExpressionPrimary
+ | InValueList ',' NonParenthesizedValueExpressionPrimary
+ ;
+
+NonParenthesizedValueExpressionPrimary
+ : UnsignedValueSpecification
+ | ColumnOrArbitraryFunctionRef             -> { types: ['COLREF'], columnReference: $1.chain }
+ | ColumnOrArbitraryFunctionRef ArbitraryFunctionRightPart
+   {
+     // We need to handle arbitrary UDFs here instead of inside UserDefinedFunction or there will be a conflict
+     // with columnReference for functions like: db.udf(foo)
+     var fn = $1.chain[$1.chain.length - 1].name.toLowerCase();
+     $1.lastLoc.type = 'function';
+     $1.lastLoc.function = fn;
+     $1.lastLoc.location = {
+       first_line: $1.lastLoc.location.first_line,
+       last_line: $1.lastLoc.location.last_line,
+       first_column: $1.lastLoc.location.first_column,
+       last_column: $1.lastLoc.location.last_column - 1
+     }
+     if ($1.lastLoc !== $1.firstLoc) {
+        $1.firstLoc.type = 'database';
+     } else {
+       delete $1.lastLoc.identifierChain;
+     }
+     if ($2.expression) {
+       $$ = { function: fn, expression: $2.expression, types: parser.findReturnTypes(fn) }
+     } else {
+       $$ = { function: fn, types: parser.findReturnTypes(fn) }
+     }
+   }
+ | ArbitraryFunctionName ArbitraryFunctionRightPart
+  {
+    parser.addFunctionLocation(@1, $1);
+    if ($2.expression) {
+      $$ = { function: $1, expression: $2.expression, types: parser.findReturnTypes($1) }
+    } else {
+      $$ = { function: $1, types: parser.findReturnTypes($1) }
+    }
+  }
+ | UserDefinedFunction
+ | 'NULL'                      -> { types: [ 'NULL' ] }
+ ;
+
+NonParenthesizedValueExpressionPrimary_EDIT
+ : UnsignedValueSpecification_EDIT
+ | ColumnOrArbitraryFunctionRef_EDIT
+   {
+     if ($1.suggestKeywords) {
+       $$ = { types: ['COLREF'], columnReference: $1, suggestKeywords: $1.suggestKeywords };
+     } else {
+       $$ = { types: ['COLREF'], columnReference: $1 };
+     }
+   }
+ | ColumnOrArbitraryFunctionRef ArbitraryFunctionRightPart_EDIT
+   {
+     var fn = $1.chain[$1.chain.length - 1].name.toLowerCase();
+     $1.lastLoc.type = 'function';
+     $1.lastLoc.function = fn;
+     $1.lastLoc.location = {
+       first_line: $1.lastLoc.location.first_line,
+       last_line: $1.lastLoc.location.last_line,
+       first_column: $1.lastLoc.location.first_column,
+       last_column: $1.lastLoc.location.last_column - 1
+     }
+     if ($1.lastLoc !== $1.firstLoc) {
+        $1.firstLoc.type = 'database';
+     } else {
+       delete $1.lastLoc.identifierChain;
+     }
+     if ($2.position) {
+       parser.applyArgumentTypesToSuggestions(fn, $2.position);
+     }
+     $$ = { types: parser.findReturnTypes(fn) };
+   }
+ | ArbitraryFunctionName ArbitraryFunctionRightPart_EDIT
+   {
+     parser.addFunctionLocation(@1, $1);
+     if ($2.position) {
+       parser.applyArgumentTypesToSuggestions($1, $2.position);
+     }
+     $$ = { types: parser.findReturnTypes($1) };
+   }
+ | UserDefinedFunction_EDIT
+ ;
+
+ColumnOrArbitraryFunctionRef
+ : BasicIdentifierChain
+   {
+     var lastLoc = parser.yy.locations[parser.yy.locations.length - 1];
+     if (lastLoc.type !== 'variable') {
+       lastLoc.type = 'column';
+     }
+     // used for function references with db prefix
+     var firstLoc = parser.yy.locations[parser.yy.locations.length - $1.length];
+     $$ = { chain: $1, firstLoc: firstLoc, lastLoc: lastLoc }
+   }
+ | BasicIdentifierChain '.' '*'
+   {
+     parser.addAsteriskLocation(@3, $1.concat({ asterisk: true }));
+   }
+ ;
+
+ColumnOrArbitraryFunctionRef_EDIT
+ : BasicIdentifierChain_EDIT
+ ;
+
+SignedInteger
+ : UnsignedNumericLiteral
+ | '-' UnsignedNumericLiteral
+ | '+' UnsignedNumericLiteral
+ ;
+
+UnsignedValueSpecification
+ : UnsignedLiteral
+ ;
+
+UnsignedValueSpecification_EDIT
+ : UnsignedLiteral_EDIT
+   {
+     parser.suggestValues($1);
+   }
+ ;
+
+UnsignedLiteral
+ : UnsignedNumericLiteral  -> { types: [ 'NUMBER' ] }
+ | GeneralLiteral
+ ;
+
+UnsignedLiteral_EDIT
+ : GeneralLiteral_EDIT
+ ;
+
+UnsignedNumericLiteral
+ : ExactNumericLiteral
+ | ApproximateNumericLiteral
+ ;
+
+ExactNumericLiteral
+ : 'UNSIGNED_INTEGER'
+ | 'UNSIGNED_INTEGER' '.'                     -> $1 + $2
+ | 'UNSIGNED_INTEGER' '.' 'UNSIGNED_INTEGER'  -> $1 + $2 + $3
+ | '.' 'UNSIGNED_INTEGER'                     -> $1 + $2
+ ;
+
+ApproximateNumericLiteral
+ : UNSIGNED_INTEGER_E 'UNSIGNED_INTEGER'
+ | '.' UNSIGNED_INTEGER_E 'UNSIGNED_INTEGER'
+ | 'UNSIGNED_INTEGER' '.' UNSIGNED_INTEGER_E 'UNSIGNED_INTEGER'
+ ;
+
+GeneralLiteral
+ : SingleQuotedValue
+   {
+     if (/\$\{[^}]*\}/.test($1)) {
+       parser.addVariableLocation(@1, $1);
+       $$ = { types: [ 'STRING' ], columnReference: [{ name: $1 }] }
+     } else {
+       $$ = { types: [ 'STRING' ] }
+     }
+   }
+ | DoubleQuotedValue
+   {
+     if (/\$\{[^}]*\}/.test($1)) {
+       parser.addVariableLocation(@1, $1);
+       $$ = { types: [ 'STRING' ], columnReference: [{ name: $1 }] }
+     } else {
+       $$ = { types: [ 'STRING' ] }
+     }
+   }
+ | TruthValue         -> { types: [ 'BOOLEAN' ] }
+ ;
+
+GeneralLiteral_EDIT
+ : SingleQuotedValue_EDIT
+  {
+    $$ = { partialQuote: '\'', missingEndQuote: parser.yy.missingEndQuote };
+  }
+ | DoubleQuotedValue_EDIT
+  {
+    $$ = { partialQuote: '"', missingEndQuote: parser.yy.missingEndQuote };
+  }
+ ;
+
+TruthValue
+ : 'TRUE'
+ | 'FALSE'
+ ;
+
+OptionalNot
+ :
+ | 'NOT'
+ ;
+
+SelectSpecification
+ : ValueExpression OptionalCorrelationName
+   {
+     if ($2) {
+       parser.addColumnAliasLocation($2.location, $2.alias, @1);
+       $$ = { valueExpression: $1, alias: $2.alias };
+       if (!parser.yy.selectListAliases) {
+         parser.yy.selectListAliases = [];
+       }
+       parser.yy.selectListAliases.push({ name: $2.alias, types: $1.types || ['T'] });
+     } else {
+       $$ = { valueExpression: $1 }
+     }
+   }
+ | '*'
+   {
+     parser.addAsteriskLocation(@1, [{ asterisk: true }]);
+     $$ = { asterisk: true }
+   }
+ ;
+
+SelectSpecification_EDIT
+ : ValueExpression_EDIT OptionalCorrelationName
+   {
+     if ($2) {
+       parser.addColumnAliasLocation($2.location, $2.alias, @1);
+     }
+   }
+
+ | AnyCursor 'AS' RegularOrBacktickedIdentifier
+   {
+     parser.suggestFunctions();
+     parser.suggestColumns();
+     parser.addColumnAliasLocation(@3, $3, @1);
+     $$ = { suggestAggregateFunctions: true };
+   }
+ | ValueExpression OptionalCorrelationName_EDIT  -> $2
+ ;
+
+SelectList
+ : SelectSpecification                 -> [ $1 ]
+ | SelectList ',' SelectSpecification
+   {
+     $1.push($3);
+   }
+ ;
+
+SelectList_EDIT
+ : SelectSpecification_EDIT
+ | 'CURSOR' SelectList
+   {
+     $$ = { cursorAtStart : true, suggestFunctions: true, suggestColumns: true, suggestAggregateFunctions: true };
+   }
+ | 'CURSOR' ',' SelectList
+   {
+     $$ = { cursorAtStart : true, suggestFunctions: true, suggestColumns: true, suggestAggregateFunctions: true };
+   }
+ | SelectSpecification_EDIT ',' SelectList
+ | SelectList 'CURSOR' SelectList
+   {
+     parser.checkForSelectListKeywords($1);
+   }
+ | SelectList 'CURSOR' ',' SelectList
+   {
+     parser.checkForSelectListKeywords($1);
+   }
+ | SelectList ',' AnyCursor
+   {
+     $$ = { suggestKeywords: parser.getSelectListKeywords(), suggestTables: true, suggestDatabases: true, suggestFunctions: true, suggestColumns: true, suggestAggregateFunctions: true };
+   }
+ | SelectList ',' SelectSpecification_EDIT                 -> $3
+ | SelectList ',' AnyCursor SelectList
+   {
+     $$ = { suggestKeywords: parser.getSelectListKeywords(), suggestFunctions: true, suggestColumns: true, suggestAggregateFunctions: true,  };
+   }
+ | SelectList ',' AnyCursor ','
+   {
+     $$ = { suggestKeywords: parser.getSelectListKeywords(), suggestFunctions: true, suggestColumns: true, suggestAggregateFunctions: true,  };
+   }
+ | SelectList ',' SelectSpecification_EDIT ','             -> $3
+ | SelectList ',' AnyCursor ',' SelectList
+   {
+     $$ = { suggestKeywords: parser.getSelectListKeywords(), suggestFunctions: true, suggestColumns: true, suggestAggregateFunctions: true,  };
+   }
+ | SelectList ',' SelectSpecification_EDIT ',' SelectList  -> $3
+ ;
+
+TableReferenceList
+ : TableReference
+ | TableReferenceList ',' TableReference  -> $3
+ ;
+
+TableReferenceList_EDIT
+ : TableReference_EDIT
+ | TableReference_EDIT ',' TableReference
+ | TableReferenceList ',' TableReference_EDIT
+ | TableReferenceList ',' TableReference_EDIT ',' TableReferenceList
+ | TableReferenceList ',' AnyCursor
+   {
+       parser.suggestTables();
+       parser.suggestDatabases({ appendDot: true });
+   }
+ ;
+
+TableReference
+ : TablePrimaryOrJoinedTable
+ ;
+
+TableReference_EDIT
+ : TablePrimaryOrJoinedTable_EDIT
+ ;
+
+TablePrimaryOrJoinedTable
+ : TablePrimary
+   {
+     $$ = $1;
+
+     if (parser.yy.latestTablePrimaries.length > 0) {
+       var idx = parser.yy.latestTablePrimaries.length - 1;
+       var tables = [];
+       do {
+         var tablePrimary = parser.yy.latestTablePrimaries[idx];
+         if (!tablePrimary.subQueryAlias) {
+           tables.unshift(tablePrimary.alias ? { identifierChain: tablePrimary.identifierChain, alias: tablePrimary.alias } : { identifierChain: tablePrimary.identifierChain })
+         }
+         idx--;
+       } while (idx >= 0 && tablePrimary.join && !tablePrimary.subQueryAlias)
+
+       if (tables.length > 0) {
+         $$.suggestJoins = {
+           prependJoin: true,
+           tables: tables
+         };
+       }
+      }
+   }
+ | JoinedTable
+ ;
+
+TablePrimaryOrJoinedTable_EDIT
+ : TablePrimary_EDIT
+ | JoinedTable_EDIT
+ ;
+
+JoinedTable
+ : TablePrimary Joins  -> $2
+ ;
+
+JoinedTable_EDIT
+ : TablePrimary Joins_EDIT
+ | TablePrimary_EDIT Joins
+ ;
+
+Joins
+ : JoinType TablePrimary OptionalJoinCondition
+   {
+     if ($3 && $3.valueExpression) {
+       $$ = $3.valueExpression;
+     } else {
+       $$ = {};
+     }
+     $$.joinType = $1;
+     if ($3.noJoinCondition) {
+       $$.suggestJoinConditions = { prependOn: true, tablePrimaries: parser.yy.latestTablePrimaries.concat() }
+     }
+     if ($3.suggestKeywords) {
+       $$.suggestKeywords = $3.suggestKeywords;
+     }
+     if (parser.yy.latestTablePrimaries.length > 0) {
+        parser.yy.latestTablePrimaries[parser.yy.latestTablePrimaries.length - 1].join = true;
+     }
+   }
+ | Joins JoinType TablePrimary OptionalJoinCondition
+   {
+     if ($4 && $4.valueExpression) {
+       $$ = $4.valueExpression;
+     } else {
+       $$ = {};
+     }
+     $$.joinType = $1;
+     if ($4.noJoinCondition) {
+       $$.suggestJoinConditions = { prependOn: true, tablePrimaries: parser.yy.latestTablePrimaries.concat() }
+     }
+     if ($4.suggestKeywords) {
+       $$.suggestKeywords = $4.suggestKeywords;
+     }
+     if (parser.yy.latestTablePrimaries.length > 0) {
+       parser.yy.latestTablePrimaries[parser.yy.latestTablePrimaries.length - 1].join = true;
+     }
+   }
+ ;
+
+Joins_INVALID
+ : JoinType                                           -> { joinType: $1 }
+ | JoinType Joins                                     -> { joinType: $1 }
+ ;
+
+Join_EDIT
+ : JoinType_EDIT TablePrimary OptionalJoinCondition
+   {
+     if ($1.suggestKeywords) {
+       parser.suggestKeywords($1.suggestKeywords);
+     }
+   }
+ | JoinType_EDIT
+   {
+     if ($1.suggestKeywords) {
+       parser.suggestKeywords($1.suggestKeywords);
+     }
+   }
+ | JoinType TablePrimary_EDIT OptionalJoinCondition
+ | JoinType TablePrimary JoinCondition_EDIT
+ | JoinType 'CURSOR' OptionalJoinCondition
+   {
+     if (parser.yy.latestTablePrimaries.length > 0) {
+       var idx = parser.yy.latestTablePrimaries.length - 1;
+       var tables = [];
+       do {
+         var tablePrimary = parser.yy.latestTablePrimaries[idx];
+         if (!tablePrimary.subQueryAlias) {
+           tables.unshift(tablePrimary.alias ? { identifierChain: tablePrimary.identifierChain, alias: tablePrimary.alias } : { identifierChain: tablePrimary.identifierChain })
+         }
+         idx--;
+       } while (idx >= 0 && tablePrimary.join && !tablePrimary.subQueryAlias)
+
+       if (tables.length > 0) {
+         parser.suggestJoins({
+           prependJoin: false,
+           joinType: $1,
+           tables: tables
+         })
+       }
+     }
+     parser.suggestTables();
+     parser.suggestDatabases({
+       appendDot: true
+     });
+   }
+ ;
+
+Joins_EDIT
+ : Join_EDIT
+ | Join_EDIT Joins
+ | Joins Join_EDIT
+ | Joins Join_EDIT Joins
+ ;
+
+JoinType
+ : 'CROSS' 'JOIN'                 -> 'CROSS JOIN'
+ | 'FULL' 'JOIN'                  -> 'FULL JOIN'
+ | 'FULL' 'OUTER' 'JOIN'          -> 'FULL OUTER JOIN'
+ | 'INNER' 'JOIN'                 -> 'INNER JOIN'
+ | 'JOIN'                         -> 'JOIN'
+ | 'LEFT' 'INNER' 'JOIN'          -> 'LEFT INNER JOIN'
+ | 'LEFT' 'JOIN'                  -> 'LEFT JOIN'
+ | 'LEFT' 'OUTER' 'JOIN'          -> 'LEFT OUTER JOIN'
+ | 'LEFT' 'SEMI' 'JOIN'           -> 'LEFT SEMI JOIN'
+ | 'OUTER' 'JOIN'                 -> 'OUTER JOIN'
+ | 'RIGHT' 'INNER' 'JOIN'         -> 'RIGHT OUTER JOIN'
+ | 'RIGHT' 'JOIN'                 -> 'RIGHT JOIN'
+ | 'RIGHT' 'OUTER' 'JOIN'         -> 'RIGHT OUTER JOIN'
+ | 'RIGHT' 'SEMI' 'JOIN'          -> 'RIGHT SEMI JOIN'
+ | 'SEMI' 'JOIN'                  -> 'SEMI JOIN'
+ ;
+
+JoinType_EDIT
+ : 'CROSS' 'CURSOR'                 -> { suggestKeywords: ['JOIN'] }
+ | 'FULL' 'CURSOR' 'JOIN'           -> { suggestKeywords: ['OUTER'] }
+ | 'FULL' 'OUTER' 'CURSOR'          -> { suggestKeywords: ['JOIN'] }
+ | 'INNER' 'CURSOR'                 -> { suggestKeywords: ['JOIN'] }
+ | 'LEFT' 'CURSOR' 'JOIN'           -> { suggestKeywords: ['OUTER'] }
+ | 'LEFT' 'INNER' 'CURSOR'          -> { suggestKeywords: ['JOIN'] }
+ | 'LEFT' 'OUTER' 'CURSOR'          -> { suggestKeywords: ['JOIN'] }
+ | 'LEFT' 'SEMI' 'CURSOR'           -> { suggestKeywords: ['JOIN'] }
+ | 'OUTER' 'CURSOR'                 -> { suggestKeywords: ['JOIN'] }
+ | 'RIGHT' 'CURSOR' 'JOIN'          -> { suggestKeywords: ['OUTER'] }
+ | 'RIGHT' 'INNER' 'CURSOR'         -> { suggestKeywords: ['JOIN'] }
+ | 'RIGHT' 'OUTER' 'CURSOR'         -> { suggestKeywords: ['JOIN'] }
+ | 'RIGHT' 'SEMI' 'CURSOR'          -> { suggestKeywords: ['JOIN'] }
+ | 'SEMI' 'CURSOR'                  -> { suggestKeywords: ['JOIN'] }
+ ;
+
+OptionalJoinCondition
+ :                                       -> { noJoinCondition: true, suggestKeywords: ['ON'] }
+ | 'ON' ValueExpression                  -> { valueExpression: $2 }
+ ;
+
+UsingColList
+ : RegularOrBacktickedIdentifier
+ | UsingColList ',' RegularOrBacktickedIdentifier
+ ;
+
+JoinCondition_EDIT
+ : 'ON' ValueExpression_EDIT
+ | 'ON' 'CURSOR'
+   {
+     parser.valueExpressionSuggest();
+     parser.suggestJoinConditions({ prependOn: false });
+   }
+ ;
+
+TablePrimary
+ : TableOrQueryName OptionalCorrelationName
+   {
+     $$ = {
+       primary: $1
+     }
+     if ($1.identifierChain) {
+       if ($2) {
+         $1.alias = $2.alias
+         parser.addTableAliasLocation($2.location, $2.alias, $1.identifierChain);
+       }
+       parser.addTablePrimary($1);
+     }
+
+     var keywords = [];
+     if (!$2) {
+       keywords = ['AS'];
+     } else if ($2.suggestKeywords) {
+       keywords = $2.suggestKeywords;
+     }
+     if (keywords.length > 0) {
+       $$.suggestKeywords = keywords;
+     }
+   }
+ | DerivedTable OptionalCorrelationName
+   {
+     $$ = {
+       primary: $1
+     };
+
+     if ($2) {
+       $$.primary.alias = $2.alias;
+       parser.addTablePrimary({ subQueryAlias: $2.alias });
+       parser.addSubqueryAliasLocation($2.location, $2.alias, $1.identifierChain);
+     }
+
+     var keywords = [];
+     if (!$2) {
+       keywords = ['AS'];
+     }
+     if (keywords.length > 0) {
+       $$.suggestKeywords = keywords;
+     }
+   }
+ ;
+
+TablePrimary_EDIT
+ : TableOrQueryName_EDIT OptionalCorrelationName
+   {
+     if ($2) {
+       parser.addTableAliasLocation($2.location, $2.alias, $1.identifierChain);
+     }
+   }
+ | DerivedTable_EDIT OptionalCorrelationName
+   {
+     if ($2) {
+       parser.addTablePrimary({ subQueryAlias: $2.alias });
+       parser.addSubqueryAliasLocation($2.location, $2.alias);
+     }
+   }
+ | DerivedTable OptionalCorrelationName_EDIT
+ ;
+
+TableOrQueryName
+ : SchemaQualifiedTableIdentifier
+ ;
+
+TableOrQueryName_EDIT
+ : SchemaQualifiedTableIdentifier_EDIT
+ ;
+
+DerivedTable
+ : TableSubQuery
+ ;
+
+DerivedTable_EDIT
+ : TableSubQuery_EDIT
+ ;
+
+OptionalOnColumn
+ :
+ | 'ON' ValueExpression
+ ;
+
+OptionalOnColumn_EDIT
+ : 'ON' 'CURSOR'
+   {
+     parser.valueExpressionSuggest();
+   }
+ | 'ON' ValueExpression_EDIT
+ ;
+
+PushQueryState
+ :
+   {
+     parser.pushQueryState();
+   }
+ ;
+
+PopQueryState
+ :
+   {
+     parser.popQueryState();
+   }
+ ;
+
+TableSubQuery
+ : '(' TableSubQueryInner ')'  -> $2
+ | '(' DerivedTable OptionalCorrelationName ')'
+   {
+     if ($3) {
+       $2.alias = $3.alias;
+       parser.addTablePrimary({ subQueryAlias: $3.alias });
+       parser.addSubqueryAliasLocation($3.location, $3.alias, $2.identifierChain);
+     }
+     $$ = $2;
+   }
+ ;
+
+TableSubQuery_EDIT
+ : '(' TableSubQueryInner_EDIT RightParenthesisOrError
+ | '(' AnyCursor RightParenthesisOrError
+   {
+     parser.suggestKeywords(['SELECT']);
+   }
+ ;
+
+TableSubQueryInner
+ : PushQueryState SubQuery
+   {
+     var subQuery = parser.getSubQuery($2);
+     subQuery.columns.forEach(function (column) {
+       parser.expandIdentifierChain({ wrapper: column });
+       delete column.linked;
+     });
+     parser.popQueryState(subQuery);
+     $$ = subQuery;
+   }
+ ;
+
+TableSubQueryInner_EDIT
+ : PushQueryState SubQuery_EDIT PopQueryState
+ ;
+
+SubQuery
+ : QueryExpression
+ ;
+
+SubQuery_EDIT
+ : QueryExpression_EDIT
+ ;
+
+QueryExpression
+ : QueryExpressionBody
+ ;
+
+QueryExpression_EDIT
+ : QueryExpressionBody_EDIT
+ ;
+
+QueryExpressionBody
+ : NonJoinQueryExpression
+ ;
+
+QueryExpressionBody_EDIT
+ : NonJoinQueryExpression_EDIT
+ ;
+
+NonJoinQueryExpression
+ : NonJoinQueryTerm
+ ;
+
+NonJoinQueryExpression_EDIT
+ : NonJoinQueryTerm_EDIT
+ ;
+
+NonJoinQueryTerm
+ : NonJoinQueryPrimary
+ ;
+
+NonJoinQueryTerm_EDIT
+ : NonJoinQueryPrimary_EDIT
+ ;
+
+NonJoinQueryPrimary
+ : SimpleTable
+ ;
+
+NonJoinQueryPrimary_EDIT
+ : SimpleTable_EDIT
+ ;
+
+SimpleTable
+ : QuerySpecification
+ ;
+
+SimpleTable_EDIT
+ : QuerySpecification_EDIT
+ ;
+
+OptionalCorrelationName
+ :
+ | RegularOrBacktickedIdentifier        -> { alias: $1, location: @1 }
+ | QuotedValue                          -> { alias: $1, location: @1 }
+ | 'AS' RegularOrBacktickedIdentifier  -> { alias: $2, location: @2 }
+ | 'AS' QuotedValue                    -> { alias: $2, location: @2 }
+ ;
+
+OptionalCorrelationName_EDIT
+ : PartialBacktickedIdentifier
+ | QuotedValue_EDIT
+ | 'AS' PartialBacktickedIdentifier
+ | 'AS' QuotedValue_EDIT
+ | 'AS' 'CURSOR'
+ ;
+
+UserDefinedFunction
+ : AggregateFunction OptionalOverClause
+   {
+     if (!$2) {
+       $1.suggestKeywords = ['OVER'];
+     }
+   }
+ | AnalyticFunction OverClause
+ | CastFunction
+ ;
+
+UserDefinedFunction_EDIT
+ : AggregateFunction_EDIT
+ | AggregateFunction OptionalOverClause_EDIT
+ | AnalyticFunction_EDIT
+ | AnalyticFunction_EDIT OverClause
+ | AnalyticFunction 'CURSOR'
+   {
+     parser.suggestKeywords(['OVER']);
+   }
+ | AnalyticFunction OverClause_EDIT
+ | CastFunction_EDIT
+ ;
+
+ArbitraryFunction
+ : RegularIdentifier ArbitraryFunctionRightPart
+   {
+     parser.addFunctionLocation(@1, $1);
+     if ($2.expression) {
+       $$ = { function: $1, expression: $2.expression, types: parser.findReturnTypes($1) }
+     } else {
+       $$ = { function: $1, types: parser.findReturnTypes($1) }
+     }
+   }
+ | ArbitraryFunctionName ArbitraryFunctionRightPart
+   {
+     parser.addFunctionLocation(@1, $1);
+     if ($2.expression) {
+       $$ = { function: $1, expression: $2.expression, types: parser.findReturnTypes($1) }
+     } else {
+       $$ = { function: $1, types: parser.findReturnTypes($1) }
+     }
+   }
+ ;
+
+ArbitraryFunction_EDIT
+ : RegularIdentifier ArbitraryFunctionRightPart_EDIT
+   {
+     parser.addFunctionLocation(@1, $1);
+     if ($2.position) {
+       parser.applyArgumentTypesToSuggestions($1, $2.position);
+     }
+     $$ = { types: parser.findReturnTypes($1) };
+   }
+ | ArbitraryFunctionName ArbitraryFunctionRightPart_EDIT
+   {
+     parser.addFunctionLocation(@1, $1);
+     if ($2.position) {
+       parser.applyArgumentTypesToSuggestions($1, $2.position);
+     }
+     $$ = { types: parser.findReturnTypes($1) };
+   }
+ ;
+
+ArbitraryFunctionName
+ : 'ARRAY'
+ | 'IF'
+ | 'MAP'
+ | 'TRUNCATE'
+ ;
+
+ArbitraryFunctionRightPart
+ : '(' ')'
+ | '(' ValueExpressionList ')'  -> { expression: $2 }
+ ;
+
+ArbitraryFunctionRightPart_EDIT
+ : '(' AnyCursor RightParenthesisOrError
+   {
+     parser.valueExpressionSuggest();
+     $$ = { position: 1 }
+   }
+ | '(' ValueExpressionList 'CURSOR' RightParenthesisOrError
+   {
+     parser.suggestValueExpressionKeywords($3);
+   }
+ | '(' ValueExpressionList_EDIT RightParenthesisOrError      -> $2
+ ;
+
+AggregateFunction
+ : CountFunction
+ | SumFunction
+ | OtherAggregateFunction
+ ;
+
+AggregateFunction_EDIT
+ : CountFunction_EDIT
+ | SumFunction_EDIT
+ | OtherAggregateFunction_EDIT
+ ;
+
+AnalyticFunction
+ : 'ANALYTIC' '(' ')'                      -> { types: parser.findReturnTypes($1) }
+ | 'ANALYTIC' '(' ValueExpressionList ')'  -> { function: $1, expression: $2, types: parser.findReturnTypes($1) }
+ ;
+
+AnalyticFunction_EDIT
+ : 'ANALYTIC' '(' AnyCursor RightParenthesisOrError
+   {
+     parser.valueExpressionSuggest();
+     parser.applyArgumentTypesToSuggestions($1, 1);
+     $$ = { types: parser.findReturnTypes($1) };
+   }
+ | 'ANALYTIC' '(' ValueExpressionList 'CURSOR' RightParenthesisOrError
+   {
+     parser.suggestValueExpressionKeywords($3);
+     $$ = { types: parser.findReturnTypes($1) };
+   }
+ | 'ANALYTIC' '(' ValueExpressionList_EDIT RightParenthesisOrError
+   {
+     parser.applyArgumentTypesToSuggestions($1, $3.position);
+     $$ = { types: parser.findReturnTypes($1) };
+   }
+ ;
+
+OptionalOverClause
+ :
+ | OverClause
+ ;
+
+OptionalOverClause_EDIT
+ : OverClause_EDIT
+ ;
+
+OverClause
+ : 'OVER' RegularOrBacktickedIdentifier
+ | 'OVER' WindowExpression
+ ;
+
+OverClause_EDIT
+ : 'OVER' WindowExpression_EDIT
+ ;
+
+WindowExpression
+ : '(' OptionalPartitionBy OptionalOrderByAndWindow ')'
+ ;
+
+WindowExpression_EDIT
+ : '(' PartitionBy_EDIT OptionalOrderByAndWindow RightParenthesisOrError
+   {
+     if (parser.yy.result.suggestFunctions) {
+       parser.suggestAggregateFunctions();
+     }
+   }
+ | '(' OptionalPartitionBy OptionalOrderByAndWindow_EDIT RightParenthesisOrError
+   {
+     if (parser.yy.result.suggestFunctions) {
+       parser.suggestAggregateFunctions();
+     }
+   }
+ | '(' AnyCursor OptionalPartitionBy OptionalOrderByAndWindow RightParenthesisOrError
+   {
+     if (!$3 && !$4) {
+       parser.suggestKeywords([{ value: 'PARTITION BY', weight: 2 }, { value: 'ORDER BY', weight: 1 }]);
+     } else if (!$3) {
+       parser.suggestKeywords(['PARTITION BY']);
+     }
+   }
+ | '(' 'PARTITION' 'BY' ValueExpressionList 'CURSOR' OptionalOrderByAndWindow RightParenthesisOrError
+    {
+      if (!$6) {
+        parser.suggestValueExpressionKeywords($4, [{ value: 'ORDER BY', weight: 2 }]);
+      } else {
+        parser.suggestValueExpressionKeywords($4);
+      }
+    }
+  ;
+
+OptionalPartitionBy
+ :
+ | PartitionBy
+ ;
+
+PartitionBy
+ : 'PARTITION' 'BY' ValueExpressionList  -> $3
+ ;
+
+PartitionBy_EDIT
+ : 'PARTITION' 'CURSOR'
+   {
+     parser.suggestKeywords(['BY']);
+   }
+ | 'PARTITION' 'BY' 'CURSOR'
+   {
+     parser.valueExpressionSuggest();
+   }
+ | 'PARTITION' 'BY' ValueExpressionList_EDIT
+ ;
+
+OptionalOrderByAndWindow
+ :
+ | OrderByClause OptionalWindowSpec
+ ;
+
+OptionalOrderByAndWindow_EDIT
+  : OrderByClause_EDIT
+    {
+      // Only allowed in last order by
+      delete parser.yy.result.suggestAnalyticFunctions;
+    }
+  | OrderByClause 'CURSOR' OptionalWindowSpec
+    {
+      var keywords = [];
+      if ($1.suggestKeywords) {
+        keywords = parser.createWeightedKeywords($1.suggestKeywords, 2);
+      }
+      if (!$3) {
+        keywords = keywords.concat([{ value: 'RANGE BETWEEN', weight: 1 }, { value: 'ROWS BETWEEN', weight: 1 }]);
+      }
+      parser.suggestKeywords(keywords);
+    }
+  | OrderByClause WindowSpec_EDIT
+  ;
+
+OptionalWindowSpec
+ :
+ | WindowSpec
+ ;
+
+WindowSpec
+ : RowsOrRange 'BETWEEN' PopLexerState OptionalCurrentOrPreceding OptionalAndFollowing
+ | RowsOrRange 'UNBOUNDED' PopLexerState OptionalCurrentOrPreceding OptionalAndFollowing
+ ;
+
+WindowSpec_EDIT
+ : RowsOrRange 'CURSOR'
+   {
+     parser.suggestKeywords(['BETWEEN']);
+   }
+ | RowsOrRange 'BETWEEN' PopLexerState OptionalCurrentOrPreceding OptionalAndFollowing 'CURSOR'
+   {
+     if (!$4 && !$5) {
+       parser.suggestKeywords(['CURRENT ROW', 'UNBOUNDED PRECEDING']);
+     } else if (!$5) {
+       parser.suggestKeywords(['AND']);
+     }
+   }
+ | RowsOrRange 'BETWEEN' PopLexerState OptionalCurrentOrPreceding_EDIT OptionalAndFollowing
+ | RowsOrRange 'BETWEEN' PopLexerState OptionalCurrentOrPreceding OptionalAndFollowing_EDIT
+ | RowsOrRange 'UNBOUNDED' PopLexerState OptionalCurrentOrPreceding 'CURSOR'
+ | RowsOrRange 'UNBOUNDED' PopLexerState OptionalCurrentOrPreceding_EDIT
+ ;
+
+PopLexerState
+ :
+  {
+    lexer.popState();
+  }
+ ;
+
+PushHdfsLexerState
+ :
+  {
+    lexer.begin('hdfs');
+  }
+ ;
+
+HdfsPath
+ : 'HDFS_START_QUOTE' 'HDFS_PATH' 'HDFS_END_QUOTE'
+ ;
+
+HdfsPath_EDIT
+ : 'HDFS_START_QUOTE' 'HDFS_PATH' 'PARTIAL_CURSOR' 'HDFS_PATH' 'HDFS_END_QUOTE'
+    {
+      parser.suggestHdfs({ path: $2 });
+    }
+ | 'HDFS_START_QUOTE' 'HDFS_PATH' 'PARTIAL_CURSOR' 'HDFS_END_QUOTE'
+   {
+     parser.suggestHdfs({ path: $2 });
+   }
+ | 'HDFS_START_QUOTE' 'HDFS_PATH' 'PARTIAL_CURSOR'
+    {
+      parser.suggestHdfs({ path: $2 });
+    }
+ | 'HDFS_START_QUOTE' 'PARTIAL_CURSOR' 'HDFS_END_QUOTE'
+   {
+     parser.suggestHdfs({ path: '' });
+   }
+ | 'HDFS_START_QUOTE' 'PARTIAL_CURSOR'
+    {
+      parser.suggestHdfs({ path: '' });
+    }
+ ;
+
+RowsOrRange
+ : 'ROWS'
+ | 'RANGE'
+ ;
+
+OptionalCurrentOrPreceding
+ :
+ | IntegerOrUnbounded 'PRECEDING'
+ | 'CURRENT' 'ROW'
+ ;
+
+OptionalCurrentOrPreceding_EDIT
+ : IntegerOrUnbounded 'CURSOR'
+   {
+     parser.suggestKeywords(['PRECEDING']);
+   }
+ | 'CURRENT' 'CURSOR'
+   {
+     parser.suggestKeywords(['ROW']);
+   }
+ ;
+
+OptionalAndFollowing
+ :
+ | 'AND' 'CURRENT' 'ROW'
+ | 'AND' IntegerOrUnbounded 'FOLLOWING'
+ ;
+
+OptionalAndFollowing_EDIT
+ : 'AND' 'CURSOR'
+   {
+     parser.suggestKeywords(['CURRENT ROW', 'UNBOUNDED FOLLOWING']);
+   }
+ | 'AND' 'CURRENT' 'CURSOR'
+   {
+     parser.suggestKeywords(['ROW']);
+   }
+ | 'AND' IntegerOrUnbounded 'CURSOR'
+   {
+     parser.suggestKeywords(['FOLLOWING']);
+   }
+ ;
+
+IntegerOrUnbounded
+ : 'UNSIGNED_INTEGER'
+ | 'UNBOUNDED'
+ ;
+
+OptionalHavingClause
+ :
+ | HavingClause
+ ;
+
+HavingClause
+ : 'HAVING' ValueExpression
+ ;
+
+HavingClause_EDIT
+ : 'HAVING' 'CURSOR'
+   {
+     parser.valueExpressionSuggest();
+     parser.suggestAggregateFunctions();
+     parser.suggestSelectListAliases(true);
+   }
+ | 'HAVING' ValueExpression_EDIT
+   {
+     parser.suggestAggregateFunctions();
+     parser.suggestSelectListAliases(true);
+   }
+ ;
+
+CastFunction
+ : 'CAST' '(' ValueExpression 'AS' PrimitiveType ')'  -> { types: [ $5.toUpperCase() ] }
+ | 'CAST' '(' ')'                                      -> { types: [ 'T' ] }
+ ;
+
+CastFunction_EDIT
+ : 'CAST' '(' AnyCursor 'AS' PrimitiveType RightParenthesisOrError
+   {
+     parser.valueExpressionSuggest();
+     $$ = { types: [ $5.toUpperCase() ] };
+   }
+ | 'CAST' '(' AnyCursor 'AS' RightParenthesisOrError
+   {
+     parser.valueExpressionSuggest();
+     $$ = { types: [ 'T' ] };
+   }
+ | 'CAST' '(' AnyCursor RightParenthesisOrError
+   {
+     parser.valueExpressionSuggest();
+     $$ = { types: [ 'T' ] };
+   }
+ | 'CAST' '(' ValueExpression_EDIT 'AS' PrimitiveType RightParenthesisOrError  -> { types: [ $5.toUpperCase() ] }
+ | 'CAST' '(' ValueExpression_EDIT 'AS' RightParenthesisOrError                -> { types: [ 'T' ] }
+ | 'CAST' '(' ValueExpression_EDIT RightParenthesisOrError                      -> { types: [ 'T' ] }
+ | 'CAST' '(' ValueExpression 'CURSOR' PrimitiveType RightParenthesisOrError
+   {
+     parser.suggestValueExpressionKeywords($3, [{ value: 'AS', weight: 2 }]);
+     $$ =  { types: [ $5.toUpperCase() ] };
+   }
+ | 'CAST' '(' ValueExpression 'CURSOR' RightParenthesisOrError
+   {
+     parser.suggestValueExpressionKeywords($3, [{ value: 'AS', weight: 2 }]);
+     $$ = { types: [ 'T' ] };
+   }
+ | 'CAST' '(' ValueExpression 'AS' 'CURSOR' RightParenthesisOrError
+   {
+     parser.suggestKeywords(parser.getTypeKeywords());
+     $$ = { types: [ 'T' ] };
+   }
+ | 'CAST' '(' 'AS' 'CURSOR' RightParenthesisOrError
+   {
+     parser.suggestKeywords(parser.getTypeKeywords());
+     $$ = { types: [ 'T' ] };
+   }
+ ;
+
+CountFunction
+ : 'COUNT' '(' '*' ')'                                        -> { types: parser.findReturnTypes($1) }
+ | 'COUNT' '(' ')'                                            -> { types: parser.findReturnTypes($1) }
+ | 'COUNT' '(' OptionalAllOrDistinct ValueExpressionList ')'  -> { types: parser.findReturnTypes($1) }
+ ;
+
+CountFunction_EDIT
+ : 'COUNT' '(' OptionalAllOrDistinct AnyCursor RightParenthesisOrError
+   {
+     parser.valueExpressionSuggest();
+     var keywords = parser.getSelectListKeywords();
+     if (!$3) {
+       keywords.push('DISTINCT');
+       if (parser.yy.result.suggestKeywords) {
+         keywords = parser.yy.result.suggestKeywords.concat(keywords);
+       }
+     }
+     parser.suggestKeywords(keywords);
+     $$ = { types: parser.findReturnTypes($1) };
+   }
+ | 'COUNT' '(' OptionalAllOrDistinct ValueExpressionList 'CURSOR' RightParenthesisOrError
+   {
+     parser.suggestValueExpressionKeywords($4);
+     $$ = { types: parser.findReturnTypes($1) };
+   }
+ | 'COUNT' '(' OptionalAllOrDistinct ValueExpressionList_EDIT RightParenthesisOrError
+   {
+     if ($4.cursorAtStart) {
+       var keywords = parser.getSelectListKeywords();
+       if (!$3) {
+         keywords.push('DISTINCT');
+       }
+       parser.suggestKeywords(keywords);
+     }
+     $$ = { types: parser.findReturnTypes($1) };
+   }
+ ;
+
+OtherAggregateFunction
+ : OtherAggregateFunction_Type '(' OptionalAllOrDistinct ')'                      -> { types: parser.findReturnTypes($1) }
+ | OtherAggregateFunction_Type '(' OptionalAllOrDistinct ValueExpressionList ')'  -> { types: parser.findReturnTypes($1) }
+ ;
+
+OtherAggregateFunction_EDIT
+ : OtherAggregateFunction_Type '(' OptionalAllOrDistinct AnyCursor RightParenthesisOrError
+   {
+     parser.valueExpressionSuggest();
+     var keywords = parser.getSelectListKeywords(true);
+     if (!$3) {
+       if ($1.toLowerCase() === 'group_concat') {
+         keywords.push('ALL');
+       } else {
+         keywords.push('DISTINCT');
+       }
+     }
+     if (parser.yy.result.suggestKeywords) {
+       keywords = parser.yy.result.suggestKeywords.concat(keywords);
+     }
+     parser.suggestKeywords(keywords);
+     parser.applyArgumentTypesToSuggestions($1, 1);
+     $$ = { types: parser.findReturnTypes($1) };
+   }
+ | OtherAggregateFunction_Type '(' OptionalAllOrDistinct ValueExpressionList 'CURSOR' RightParenthesisOrError
+   {
+     parser.suggestValueExpressionKeywords($4);
+     $$ = { types: parser.findReturnTypes($1) };
+   }
+ | OtherAggregateFunction_Type '(' OptionalAllOrDistinct ValueExpressionList_EDIT RightParenthesisOrError
+   {
+     if ($4.cursorAtStart) {
+       var keywords = parser.getSelectListKeywords(true);
+       if (!$3) {
+         if ($1.toLowerCase() === 'group_concat') {
+           keywords.push('ALL');
+         } else {
+           keywords.push('DISTINCT');
+         }
+       }
+       if (parser.yy.result.suggestKeywords) {
+         keywords = parser.yy.result.suggestKeywords.concat(keywords);
+       }
+       parser.suggestKeywords(keywords);
+     }
+     if (parser.yy.result.suggestFunctions && !parser.yy.result.suggestFunctions.types) {
+       parser.applyArgumentTypesToSuggestions($1, $4.position);
+     }
+     $$ = { types: parser.findReturnTypes($1) };
+   }
+ ;
+
+OtherAggregateFunction_Type
+ : 'AVG'
+ | 'MAX'
+ | 'MIN'
+ | 'STDDEV_POP'
+ | 'STDDEV_SAMP'
+ | 'VAR_POP'
+ | 'VAR_SAMP'
+ | 'VARIANCE'
+ ;
+
+FromOrComma
+ : 'FROM'
+ | ','
+ ;
+
+SumFunction
+ : 'SUM' '(' OptionalAllOrDistinct ValueExpression ')'  -> { types: parser.findReturnTypes($1) }
+ | 'SUM' '(' ')'                                        -> { types: parser.findReturnTypes($1) }
+ ;
+
+SumFunction_EDIT
+ : 'SUM' '(' OptionalAllOrDistinct AnyCursor RightParenthesisOrError
+   {
+     parser.valueExpressionSuggest();
+     parser.applyArgumentTypesToSuggestions($1, 1);
+     var keywords = parser.getSelectListKeywords(true);
+     if (!$3) {
+       keywords.push('DISTINCT');
+     }
+     if (parser.yy.result.suggestKeywords) {
+       keywords = parser.yy.result.suggestKeywords.concat(keywords);
+     }
+     parser.suggestKeywords(keywords);
+     $$ = { types: parser.findReturnTypes($1) };
+   }
+ | 'SUM' '(' OptionalAllOrDistinct ValueExpression 'CURSOR' RightParenthesisOrError
+   {
+     parser.suggestValueExpressionKeywords($4);
+     $$ = { types: parser.findReturnTypes($1) };
+   }
+ | 'SUM' '(' OptionalAllOrDistinct ValueExpression_EDIT RightParenthesisOrError
+   {
+     if (parser.yy.result.suggestFunctions && ! parser.yy.result.suggestFunctions.types) {
+       parser.applyArgumentTypesToSuggestions($1, 1);
+     }
+     $$ = { types: parser.findReturnTypes($1) };
+   }
+ ;

+ 46 - 0
desktop/core/src/desktop/js/parse/jison/sql/ksql/sql_set.jison

@@ -0,0 +1,46 @@
+// 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.
+
+DataDefinition
+ : SetSpecification
+ ;
+
+DataDefinition_EDIT
+ : 'SET' 'CURSOR'
+   {
+     parser.suggestSetOptions();
+   }
+ ;
+
+SetSpecification
+ : 'SET' SetOption '=' SetValue
+ | 'SET' 'ALL'
+ ;
+
+SetOption
+ : RegularIdentifier
+ | SetOption '.' RegularIdentifier
+ ;
+
+SetValue
+ : RegularIdentifier
+ | SignedInteger
+ | SignedInteger RegularIdentifier
+ | QuotedValue
+ | 'TRUE'
+ | 'FALSE'
+ | 'NULL'
+ ;

+ 122 - 0
desktop/core/src/desktop/js/parse/jison/sql/ksql/sql_update.jison

@@ -0,0 +1,122 @@
+// 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.
+
+DataManipulation
+ : UpdateStatement
+ ;
+
+DataManipulation_EDIT
+ : UpdateStatement_EDIT
+ ;
+
+UpdateStatement
+ : 'UPDATE' TargetTable 'SET' SetClauseList OptionalFromJoinedTable OptionalWhereClause
+ ;
+
+UpdateStatement_EDIT
+ : 'UPDATE' TargetTable_EDIT 'SET' SetClauseList OptionalFromJoinedTable OptionalWhereClause
+ | 'UPDATE' TargetTable 'SET' SetClauseList_EDIT OptionalFromJoinedTable OptionalWhereClause
+ | 'UPDATE' TargetTable 'SET' SetClauseList FromJoinedTable_EDIT OptionalWhereClause
+ | 'UPDATE' TargetTable 'SET' SetClauseList OptionalFromJoinedTable WhereClause_EDIT
+ | 'UPDATE' TargetTable 'SET' SetClauseList OptionalFromJoinedTable OptionalWhereClause 'CURSOR'
+   {
+     parser.suggestKeywords([ 'WHERE' ]);
+   }
+ | 'UPDATE' TargetTable 'CURSOR'
+   {
+     parser.suggestKeywords([ 'SET' ]);
+   }
+ | 'UPDATE' TargetTable_EDIT
+ | 'UPDATE' TargetTable
+ | 'UPDATE' 'CURSOR'
+   {
+     parser.suggestTables();
+     parser.suggestDatabases({ appendDot: true });
+   }
+ ;
+
+TargetTable
+ : TableName
+ ;
+
+TargetTable_EDIT
+ : TableName_EDIT
+ ;
+
+TableName
+ : LocalOrSchemaQualifiedName
+   {
+     parser.addTablePrimary($1);
+   }
+ ;
+
+TableName_EDIT
+ : LocalOrSchemaQualifiedName_EDIT
+ ;
+
+SetClauseList
+ : SetClause
+ | SetClauseList ',' SetClause
+ ;
+
+SetClauseList_EDIT
+ : SetClause_EDIT
+ | SetClauseList ',' SetClause_EDIT
+ | SetClause_EDIT ',' SetClauseList
+ | SetClauseList ',' SetClause_EDIT ',' SetClauseList
+ ;
+
+SetClause
+ : SetTarget '=' UpdateSource
+ ;
+
+SetClause_EDIT
+ : SetTarget '=' UpdateSource_EDIT
+ | SetTarget 'CURSOR'
+   {
+     parser.suggestKeywords([ '=' ]);
+   }
+ | 'CURSOR'
+   {
+     parser.suggestColumns();
+   }
+ ;
+
+SetTarget
+ : ColumnReference
+ ;
+
+UpdateSource
+ : ValueExpression
+ ;
+
+UpdateSource_EDIT
+ : ValueExpression_EDIT
+ ;
+
+OptionalFromJoinedTable
+ :
+ | 'FROM' TableReference  -> $2
+ ;
+
+FromJoinedTable_EDIT
+ : 'FROM' 'CURSOR'
+   {
+     parser.suggestTables();
+     parser.suggestDatabases({ appendDot: true });
+   }
+ | 'FROM' TableReference_EDIT
+ ;

+ 39 - 0
desktop/core/src/desktop/js/parse/jison/sql/ksql/sql_use.jison

@@ -0,0 +1,39 @@
+// 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.
+
+DataDefinition
+ : UseStatement
+ ;
+
+DataDefinition_EDIT
+ : UseStatement_EDIT
+ ;
+
+UseStatement
+ : 'USE' RegularIdentifier
+   {
+     if (! parser.yy.cursorFound) {
+       parser.yy.result.useDatabase = $2;
+     }
+   }
+ ;
+
+UseStatement_EDIT
+ : 'USE' 'CURSOR'
+   {
+     parser.suggestDatabases();
+   }
+ ;

+ 839 - 0
desktop/core/src/desktop/js/parse/jison/sql/ksql/sql_valueExpression.jison

@@ -0,0 +1,839 @@
+// 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.
+
+ValueExpression
+ : 'NOT' ValueExpression
+   {
+     // verifyType($2, 'BOOLEAN');
+     $$ = { types: [ 'BOOLEAN' ] };
+   }
+ | '!' ValueExpression
+   {
+     // verifyType($2, 'BOOLEAN');
+     $$ = { types: [ 'BOOLEAN' ] };
+   }
+ | '~' ValueExpression                                                 -> $2
+ | '-' ValueExpression %prec NEGATION
+   {
+     // verifyType($2, 'NUMBER');
+     $$ = $2;
+     $2.types = ['NUMBER'];
+   }
+ | ValueExpression 'IS' OptionalNot 'NULL'                             -> { types: [ 'BOOLEAN' ] }
+ | ValueExpression 'IS' OptionalNot 'TRUE'                             -> { types: [ 'BOOLEAN' ] }
+ | ValueExpression 'IS' OptionalNot 'FALSE'                            -> { types: [ 'BOOLEAN' ] }
+ | ValueExpression 'IS' OptionalNot 'DISTINCT' 'FROM' ValueExpression  -> { types: [ 'BOOLEAN' ] }
+ ;
+
+ValueExpression_EDIT
+ : 'NOT' ValueExpression_EDIT                           -> { types: [ 'BOOLEAN' ], suggestFilters: $2.suggestFilters }
+ | 'NOT' 'CURSOR'
+   {
+     parser.suggestFunctions();
+     parser.suggestColumns();
+     parser.suggestKeywords(['EXISTS']);
+     $$ = { types: [ 'BOOLEAN' ] };
+   }
+ | '!' ValueExpression_EDIT                             -> { types: [ 'BOOLEAN' ], suggestFilters: $2.suggestFilters }
+ | '!' AnyCursor
+   {
+     parser.suggestFunctions({ types: [ 'BOOLEAN' ] });
+     parser.suggestColumns({ types: [ 'BOOLEAN' ] });
+     $$ = { types: [ 'BOOLEAN' ] };
+   }
+ | '~' ValueExpression_EDIT                             -> { types: [ 'T' ], suggestFilters: $2.suggestFilters }
+ | '~' 'PARTIAL_CURSOR'
+   {
+     parser.suggestFunctions();
+     parser.suggestColumns();
+     $$ = { types: [ 'T' ] };
+   }
+ | '-' ValueExpression_EDIT %prec NEGATION
+   {
+     if (!$2.typeSet) {
+       parser.applyTypeToSuggestions('NUMBER');
+     }
+     $$ = { types: [ 'NUMBER' ], suggestFilters: $2.suggestFilters };
+   }
+ | '-' 'PARTIAL_CURSOR' %prec NEGATION
+   {
+     parser.suggestFunctions({ types: [ 'NUMBER' ] });
+     parser.suggestColumns({ types: [ 'NUMBER' ] });
+     $$ = { types: [ 'NUMBER' ] };
+   }
+ | ValueExpression 'IS' 'CURSOR'
+   {
+     parser.suggestKeywords(['FALSE', 'NOT NULL', 'NOT TRUE', 'NOT FALSE', 'NULL', 'TRUE']);
+     $$ = { types: [ 'BOOLEAN' ] };
+   }
+ | ValueExpression 'IS' 'NOT' 'CURSOR'
+   {
+     parser.suggestKeywords(['FALSE', 'NULL', 'TRUE']);
+     $$ = { types: [ 'BOOLEAN' ] };
+   }
+ | ValueExpression 'IS' OptionalNot 'DISTINCT' 'CURSOR'
+   {
+     $$ = { types: [ 'BOOLEAN' ] };
+   }
+ | ValueExpression 'IS' 'CURSOR' 'NULL'
+   {
+     parser.suggestKeywords(['NOT']);
+     $$ = { types: [ 'BOOLEAN' ] };
+   }
+ | ValueExpression 'IS' 'CURSOR' 'FALSE'
+   {
+     parser.suggestKeywords(['NOT']);
+     $$ = { types: [ 'BOOLEAN' ] };
+   }
+ | ValueExpression 'IS' 'CURSOR' 'TRUE'
+   {
+     parser.suggestKeywords(['NOT']);
+     $$ = { types: [ 'BOOLEAN' ] };
+   }
+ | ValueExpression 'IS' OptionalNot 'DISTINCT' 'FROM' PartialBacktickedOrAnyCursor
+   {
+     parser.valueExpressionSuggest($1, $3 ? 'IS NOT DISTINCT FROM' : 'IS DISTINCT FROM');
+     $$ = { types: [ 'BOOLEAN' ] };
+   }
+ | ValueExpression 'IS' OptionalNot 'DISTINCT' 'FROM' ValueExpression_EDIT
+   {
+     $$ = { types: [ 'BOOLEAN' ], suggestFilters: $6.suggestFilters }
+   }
+ ;
+
+// ------------------  EXISTS and parenthesized ------------------
+ValueExpression
+ : 'EXISTS' TableSubQuery
+   {
+     $$ = { types: [ 'BOOLEAN' ] };
+     // clear correlated flag after completed sub-query (set by lexer)
+     parser.yy.correlatedSubQuery = false;
+   }
+ | '(' ValueExpression ')'                                -> $2
+ ;
+
+ValueExpression_EDIT
+ : 'EXISTS' TableSubQuery_EDIT                               -> { types: [ 'BOOLEAN' ] }
+ | '(' ValueExpression_EDIT RightParenthesisOrError
+   {
+     $$ = $2;
+   }
+ | '(' 'CURSOR' RightParenthesisOrError
+   {
+     parser.valueExpressionSuggest();
+     $$ = { types: ['T'], typeSet: true };
+   }
+ ;
+
+// ------------------  COMPARISON ------------------
+
+ValueExpression
+ : ValueExpression '=' ValueExpression
+   {
+     parser.addColRefToVariableIfExists($1, $3);
+     $$ = { types: [ 'BOOLEAN' ] };
+   }
+ | ValueExpression '<' ValueExpression
+   {
+     parser.addColRefToVariableIfExists($1, $3);
+     $$ = { types: [ 'BOOLEAN' ] };
+   }
+ | ValueExpression '>' ValueExpression
+   {
+     parser.addColRefToVariableIfExists($1, $3);
+     $$ = { types: [ 'BOOLEAN' ] };
+   }
+ | ValueExpression 'COMPARISON_OPERATOR' ValueExpression
+   {
+     parser.addColRefToVariableIfExists($1, $3);
+     $$ = { types: [ 'BOOLEAN' ] };
+   }
+ ;
+
+ValueExpression_EDIT
+ : 'CURSOR' '=' ValueExpression
+   {
+     parser.valueExpressionSuggest($3, $2);
+     parser.applyTypeToSuggestions($3.types);
+     $$ = { types: [ 'BOOLEAN' ], typeSet: true };
+   }
+ | 'CURSOR' '<' ValueExpression
+   {
+     parser.valueExpressionSuggest($3, $2);
+     parser.applyTypeToSuggestions($3.types);
+     $$ = { types: [ 'BOOLEAN' ], typeSet: true  };
+   }
+ | 'CURSOR' '>' ValueExpression
+   {
+     parser.valueExpressionSuggest($3, $2);
+     parser.applyTypeToSuggestions($3.types);
+     $$ = { types: [ 'BOOLEAN' ], typeSet: true  };
+   }
+ | 'CURSOR' 'COMPARISON_OPERATOR' ValueExpression
+   {
+     parser.valueExpressionSuggest($3, $2);
+     parser.applyTypeToSuggestions($3.types);
+     $$ = { types: [ 'BOOLEAN' ], typeSet: true  };
+   }
+ | ValueExpression_EDIT '=' ValueExpression
+   {
+     if (!$1.typeSet) {
+       parser.applyTypeToSuggestions($3.types);
+       parser.addColRefIfExists($3);
+     }
+     $$ = { types: [ 'BOOLEAN' ], suggestFilters: $1.suggestFilters }
+   }
+ | ValueExpression_EDIT '<' ValueExpression
+   {
+     if (!$1.typeSet) {
+       parser.applyTypeToSuggestions($3.types);
+       parser.addColRefIfExists($3);
+     }
+     $$ = { types: [ 'BOOLEAN' ], suggestFilters: $1.suggestFilters }
+   }
+ | ValueExpression_EDIT '>' ValueExpression
+   {
+     if (!$1.typeSet) {
+       parser.applyTypeToSuggestions($3.types);
+       parser.addColRefIfExists($3);
+     }
+     $$ = { types: [ 'BOOLEAN' ], suggestFilters: $1.suggestFilters }
+   }
+ | ValueExpression_EDIT 'COMPARISON_OPERATOR' ValueExpression
+   {
+     if (!$1.typeSet) {
+       parser.applyTypeToSuggestions($3.types);
+       parser.addColRefIfExists($3);
+     }
+     $$ = { types: [ 'BOOLEAN' ], suggestFilters: $1.suggestFilters }
+   }
+ | ValueExpression '=' PartialBacktickedOrAnyCursor
+   {
+     parser.valueExpressionSuggest($1, $2);
+     parser.applyTypeToSuggestions($1.types);
+     $$ = { types: [ 'BOOLEAN' ], typeSet: true  };
+   }
+ | ValueExpression '<' PartialBacktickedOrAnyCursor
+   {
+     parser.valueExpressionSuggest($1, $2);
+     parser.applyTypeToSuggestions($1.types);
+     $$ = { types: [ 'BOOLEAN' ] , typeSet: true, endsWithLessThanOrEqual: true };
+   }
+ | ValueExpression '>' PartialBacktickedOrAnyCursor
+   {
+     parser.valueExpressionSuggest($1, $2);
+     parser.applyTypeToSuggestions($1.types);
+     $$ = { types: [ 'BOOLEAN' ], typeSet: true  };
+   }
+ | ValueExpression 'COMPARISON_OPERATOR' PartialBacktickedOrAnyCursor
+   {
+     parser.valueExpressionSuggest($1, $2);
+     parser.applyTypeToSuggestions($1.types);
+     $$ = { types: [ 'BOOLEAN' ], typeSet: true, endsWithLessThanOrEqual: $2 === '<='  };
+   }
+ | ValueExpression '=' ValueExpression_EDIT
+   {
+     if (!$3.typeSet) {
+       parser.applyTypeToSuggestions($1.types);
+       parser.addColRefIfExists($1);
+     }
+     $$ = { types: [ 'BOOLEAN' ], suggestFilters: $3.suggestFilters }
+   }
+ | ValueExpression '<' ValueExpression_EDIT
+   {
+     if (!$3.typeSet) {
+       parser.applyTypeToSuggestions($1.types);
+       parser.addColRefIfExists($1);
+     }
+     $$ = { types: [ 'BOOLEAN' ], suggestFilters: $3.suggestFilters }
+   }
+ | ValueExpression '>' ValueExpression_EDIT
+   {
+     if (!$3.typeSet) {
+       parser.applyTypeToSuggestions($1.types);
+       parser.addColRefIfExists($1);
+     }
+     $$ = { types: [ 'BOOLEAN' ], suggestFilters: $3.suggestFilters }
+   }
+ | ValueExpression 'COMPARISON_OPERATOR' ValueExpression_EDIT
+   {
+     if (!$3.typeSet) {
+       parser.applyTypeToSuggestions($1.types);
+       parser.addColRefIfExists($1);
+     }
+     $$ = { types: [ 'BOOLEAN' ], suggestFilters: $3.suggestFilters }
+   }
+ ;
+
+
+// ------------------  IN ------------------
+
+ValueExpression
+ : ValueExpression 'NOT' 'IN' '(' TableSubQueryInner ')'   -> { types: [ 'BOOLEAN' ] }
+ | ValueExpression 'NOT' 'IN' '(' ValueExpressionList ')'  -> { types: [ 'BOOLEAN' ] }
+ | ValueExpression 'IN' '(' TableSubQueryInner ')'         -> { types: [ 'BOOLEAN' ] }
+ | ValueExpression 'IN' '(' ValueExpressionList ')'        -> { types: [ 'BOOLEAN' ] }
+ ;
+
+ValueExpression_EDIT
+ : ValueExpression 'NOT' 'IN' ValueExpressionInSecondPart_EDIT
+   {
+     if ($4.inValueEdit) {
+       parser.valueExpressionSuggest($1, $2 + ' ' + $3);
+       parser.applyTypeToSuggestions($1.types);
+     }
+     if ($4.cursorAtStart) {
+       parser.suggestKeywords(['SELECT']);
+     }
+     $$ = { types: [ 'BOOLEAN' ], typeSet: true  };
+   }
+ | ValueExpression 'IN' ValueExpressionInSecondPart_EDIT
+   {
+     if ($3.inValueEdit) {
+       parser.valueExpressionSuggest($1, $2);
+       parser.applyTypeToSuggestions($1.types);
+     }
+     if ($3.cursorAtStart) {
+       parser.suggestKeywords(['SELECT']);
+     }
+     $$ = { types: [ 'BOOLEAN' ], typeSet: true  };
+   }
+ | ValueExpression_EDIT 'NOT' 'IN' '(' ValueExpressionList RightParenthesisOrError  -> { types: [ 'BOOLEAN' ], suggestFilters: $1.suggestFilters }
+ | ValueExpression_EDIT 'NOT' 'IN' '(' TableSubQueryInner RightParenthesisOrError   -> { types: [ 'BOOLEAN' ], suggestFilters: $1.suggestFilters }
+ | ValueExpression_EDIT 'IN' '(' ValueExpressionList RightParenthesisOrError        -> { types: [ 'BOOLEAN' ], suggestFilters: $1.suggestFilters }
+ | ValueExpression_EDIT 'IN' '(' TableSubQueryInner RightParenthesisOrError         -> { types: [ 'BOOLEAN' ], suggestFilters: $1.suggestFilters }
+ ;
+
+ValueExpressionInSecondPart_EDIT
+ : '(' TableSubQueryInner_EDIT RightParenthesisOrError
+ | '(' ValueExpressionList_EDIT RightParenthesisOrError -> { inValueEdit: true }
+ | '(' AnyCursor RightParenthesisOrError                -> { inValueEdit: true, cursorAtStart: true }
+ ;
+
+// ------------------  BETWEEN ------------------
+
+ValueExpression
+ : ValueExpression 'NOT' 'BETWEEN' ValueExpression 'BETWEEN_AND' ValueExpression  -> { types: [ 'BOOLEAN' ] }
+ | ValueExpression 'BETWEEN' ValueExpression 'BETWEEN_AND' ValueExpression        -> { types: [ 'BOOLEAN' ] }
+ ;
+
+ValueExpression_EDIT
+ : ValueExpression_EDIT 'NOT' 'BETWEEN' ValueExpression 'BETWEEN_AND' ValueExpression
+   {
+     if ($4.types[0] === $6.types[0] && !$1.typeSet) {
+       parser.applyTypeToSuggestions($4.types);
+     }
+     $$ = { types: [ 'BOOLEAN' ], suggestFilters: $1.suggestFilters };
+   }
+ | ValueExpression 'NOT' 'BETWEEN' ValueExpression_EDIT 'BETWEEN_AND' ValueExpression
+   {
+     if ($1.types[0] === $6.types[0] && !$4.typeSet) {
+       parser.applyTypeToSuggestions($1.types);
+     }
+     $$ = { types: [ 'BOOLEAN' ], suggestFilters: $4.suggestFilters };
+   }
+ | ValueExpression 'NOT' 'BETWEEN' ValueExpression 'BETWEEN_AND' ValueExpression_EDIT
+   {
+     if ($1.types[0] === $4.types[0] && !$6.typeSet) {
+       parser.applyTypeToSuggestions($1.types);
+     }
+     $$ = { types: [ 'BOOLEAN' ], suggestFilters: $6.suggestFilters };
+   }
+ | ValueExpression 'NOT' 'BETWEEN' ValueExpression 'BETWEEN_AND' 'CURSOR'
+   {
+     parser.valueExpressionSuggest($1, $5);
+     $$ = { types: [ 'BOOLEAN' ], typeSet: true  };
+   }
+ | ValueExpression 'NOT' 'BETWEEN' ValueExpression 'CURSOR'
+   {
+     parser.suggestValueExpressionKeywords($4, ['AND']);
+     $$ = { types: [ 'BOOLEAN' ] };
+   }
+ | ValueExpression 'NOT' 'BETWEEN' 'CURSOR'
+   {
+     parser.valueExpressionSuggest($1, $2 + ' ' + $3);
+     $$ = { types: [ 'BOOLEAN' ], typeSet: true  };
+   }
+ | ValueExpression_EDIT 'BETWEEN' ValueExpression 'BETWEEN_AND' ValueExpression
+   {
+     if ($1.types[0] === $3.types[0] && !$1.typeSet) {
+       parser.applyTypeToSuggestions($1.types)
+     }
+     $$ = { types: [ 'BOOLEAN' ], suggestFilters: $1.suggestFilters };
+   }
+ | ValueExpression 'BETWEEN' ValueExpression_EDIT 'BETWEEN_AND' ValueExpression
+   {
+     if ($1.types[0] === $3.types[0] && !$3.typeSet) {
+       parser.applyTypeToSuggestions($1.types)
+     }
+     $$ = { types: [ 'BOOLEAN' ], suggestFilters: $3.suggestFilters };
+   }
+ | ValueExpression 'BETWEEN' ValueExpression 'BETWEEN_AND' ValueExpression_EDIT
+   {
+     if ($1.types[0] === $3.types[0] && !$5.typeSet) {
+       parser.applyTypeToSuggestions($1.types)
+     }
+     $$ = { types: [ 'BOOLEAN' ], suggestFilters: $5.suggestFilters };
+   }
+ | ValueExpression 'BETWEEN' ValueExpression 'BETWEEN_AND' 'CURSOR'
+   {
+     parser.valueExpressionSuggest($1, $4);
+     parser.applyTypeToSuggestions($1.types);
+     $$ = { types: [ 'BOOLEAN' ], typeSet: true  };
+   }
+ | ValueExpression 'BETWEEN' ValueExpression 'CURSOR'
+   {
+     parser.suggestValueExpressionKeywords($3, ['AND']);
+     $$ = { types: [ 'BOOLEAN' ] };
+   }
+ | ValueExpression 'BETWEEN' 'CURSOR'
+   {
+     parser.valueExpressionSuggest($1, $2);
+     parser.applyTypeToSuggestions($1.types);
+     $$ = { types: [ 'BOOLEAN' ], typeSet: true };
+   }
+ ;
+
+// ------------------  BOOLEAN ------------------
+
+ValueExpression
+ : ValueExpression 'OR' ValueExpression
+   {
+     // verifyType($1, 'BOOLEAN');
+     // verifyType($3, 'BOOLEAN');
+     $$ = { types: [ 'BOOLEAN' ] };
+   }
+ | ValueExpression 'AND' ValueExpression
+   {
+     // verifyType($1, 'BOOLEAN');
+     // verifyType($3, 'BOOLEAN');
+     $$ = { types: [ 'BOOLEAN' ] };
+   }
+ ;
+
+ValueExpression_EDIT
+ : 'CURSOR' 'OR' ValueExpression
+   {
+     parser.valueExpressionSuggest(undefined, $2);
+     $$ = { types: [ 'BOOLEAN' ], typeSet: true, suggestFilters: true };
+   }
+ | ValueExpression_EDIT 'OR' ValueExpression
+   {
+     parser.addColRefIfExists($3);
+     $$ = { types: [ 'BOOLEAN' ], suggestFilters: $1.suggestFilters }
+   }
+ | ValueExpression 'OR' PartialBacktickedOrAnyCursor
+   {
+     parser.valueExpressionSuggest(undefined, $2);
+     $$ = { types: [ 'BOOLEAN' ], typeSet: true, suggestFilters: true };
+   }
+ | ValueExpression 'OR' ValueExpression_EDIT
+   {
+     parser.addColRefIfExists($1);
+     $$ = { types: [ 'BOOLEAN' ], suggestFilters: $3.suggestFilters }
+   }
+ | 'CURSOR' 'AND' ValueExpression
+   {
+     parser.valueExpressionSuggest(undefined, $2);
+     $$ = { types: [ 'BOOLEAN' ], typeSet: true, suggestFilters: true };
+   }
+ | ValueExpression_EDIT 'AND' ValueExpression
+   {
+     parser.addColRefIfExists($3);
+     $$ = { types: [ 'BOOLEAN' ], suggestFilters: $1.suggestFilters }
+   }
+ | ValueExpression 'AND' PartialBacktickedOrAnyCursor
+   {
+     parser.valueExpressionSuggest(undefined, $2);
+     $$ = { types: [ 'BOOLEAN' ], typeSet: true, suggestFilters: true };
+   }
+ | ValueExpression 'AND' ValueExpression_EDIT
+   {
+     parser.addColRefIfExists($1);
+     $$ = { types: [ 'BOOLEAN' ], suggestFilters: $3.suggestFilters }
+   }
+ ;
+
+// ------------------  ARITHMETIC ------------------
+
+ValueExpression
+ : ValueExpression '-' ValueExpression
+   {
+     // verifyType($1, 'NUMBER');
+     // verifyType($3, 'NUMBER');
+     $$ = { types: [ 'NUMBER' ] };
+   }
+ | ValueExpression '*' ValueExpression
+   {
+     // verifyType($1, 'NUMBER');
+     // verifyType($3, 'NUMBER');
+     $$ = { types: [ 'NUMBER' ] };
+   }
+ | ValueExpression 'ARITHMETIC_OPERATOR' ValueExpression
+   {
+     // verifyType($1, 'NUMBER');
+     // verifyType($3, 'NUMBER');
+     $$ = { types: [ 'NUMBER' ] };
+   }
+ ;
+
+ValueExpression_EDIT
+ : 'CURSOR' '*' ValueExpression
+   {
+     parser.valueExpressionSuggest(undefined, $2);
+     parser.applyTypeToSuggestions([ 'NUMBER' ]);
+     $$ = { types: [ 'NUMBER' ], typeSet: true };
+   }
+ | 'CURSOR' 'ARITHMETIC_OPERATOR' ValueExpression
+   {
+     parser.valueExpressionSuggest(undefined, $2);
+     parser.applyTypeToSuggestions([ 'NUMBER' ]);
+     $$ = { types: [ 'NUMBER' ], typeSet: true };
+   }
+ | ValueExpression_EDIT '-' ValueExpression
+   {
+     if (!$1.typeSet) {
+       parser.applyTypeToSuggestions(['NUMBER']);
+       parser.addColRefIfExists($3);
+     }
+     $$ = { types: [ 'NUMBER' ], suggestFilters: $1.suggestFilters }
+   }
+ | ValueExpression_EDIT '*' ValueExpression
+   {
+     if (!$1.typeSet) {
+       parser.applyTypeToSuggestions(['NUMBER']);
+       parser.addColRefIfExists($3);
+     }
+     $$ = { types: [ 'NUMBER' ], suggestFilters: $1.suggestFilters }
+   }
+ | ValueExpression_EDIT 'ARITHMETIC_OPERATOR' ValueExpression
+   {
+     if (!$1.typeSet) {
+       parser.applyTypeToSuggestions(['NUMBER']);
+       parser.addColRefIfExists($3);
+     }
+     $$ = { types: [ 'NUMBER' ], suggestFilters: $1.suggestFilters }
+   }
+ | ValueExpression '-' PartialBacktickedOrAnyCursor
+   {
+     parser.valueExpressionSuggest(undefined, $2);
+     parser.applyTypeToSuggestions(['NUMBER']);
+     $$ = { types: [ 'NUMBER' ], typeSet: true };
+   }
+ | ValueExpression '*' PartialBacktickedOrAnyCursor
+   {
+     parser.valueExpressionSuggest(undefined, $2);
+     parser.applyTypeToSuggestions(['NUMBER']);
+     $$ = { types: [ 'NUMBER' ], typeSet: true };
+   }
+ | ValueExpression 'ARITHMETIC_OPERATOR' PartialBacktickedOrAnyCursor
+   {
+     parser.valueExpressionSuggest(undefined, $2);
+     parser.applyTypeToSuggestions(['NUMBER']);
+     $$ = { types: [ 'NUMBER' ], typeSet: true };
+   }
+ | ValueExpression '-' ValueExpression_EDIT
+   {
+     if (!$3.typeSet) {
+       parser.applyTypeToSuggestions(['NUMBER']);
+       parser.addColRefIfExists($1);
+     }
+     $$ = { types: [ 'NUMBER' ], suggestFilters: $3.suggestFilters };
+   }
+ | ValueExpression '*' ValueExpression_EDIT
+   {
+     if (!$3.typeSet) {
+       parser.applyTypeToSuggestions(['NUMBER']);
+       parser.addColRefIfExists($1);
+     }
+     $$ = { types: [ 'NUMBER' ], suggestFilters: $3.suggestFilters };
+   }
+ | ValueExpression 'ARITHMETIC_OPERATOR' ValueExpression_EDIT
+   {
+     if (!$3.typeSet) {
+       parser.applyTypeToSuggestions(['NUMBER']);
+       parser.addColRefIfExists($1);
+     }
+     $$ = { types: [ 'NUMBER' ], suggestFilters: $3.suggestFilters };
+   }
+ ;
+
+// ------------------  LIKE, RLIKE and REGEXP ------------------
+
+ValueExpression
+ : ValueExpression LikeRightPart          -> { types: [ 'BOOLEAN' ] }
+ | ValueExpression 'NOT' LikeRightPart    -> { types: [ 'BOOLEAN' ] }
+ ;
+
+LikeRightPart
+ : 'LIKE' ValueExpression             -> { suggestKeywords: ['NOT'] }
+ | 'RLIKE' ValueExpression            -> { suggestKeywords: ['NOT'] }
+ | 'REGEXP' ValueExpression           -> { suggestKeywords: ['NOT'] }
+ ;
+
+LikeRightPart_EDIT
+ : 'LIKE' ValueExpression_EDIT
+ | 'RLIKE' ValueExpression_EDIT
+ | 'REGEXP' ValueExpression_EDIT
+ | 'LIKE' PartialBacktickedOrCursor
+   {
+     parser.suggestFunctions({ types: [ 'STRING' ] });
+     parser.suggestColumns({ types: [ 'STRING' ] });
+     $$ = { types: ['BOOLEAN'] }
+   }
+ | 'RLIKE' PartialBacktickedOrCursor
+   {
+     parser.suggestFunctions({ types: [ 'STRING' ] });
+     parser.suggestColumns({ types: [ 'STRING' ] });
+     $$ = { types: ['BOOLEAN'] }
+   }
+ | 'REGEXP' PartialBacktickedOrCursor
+   {
+     parser.suggestFunctions({ types: [ 'STRING' ] });
+     parser.suggestColumns({ types: [ 'STRING' ] });
+     $$ = { types: ['BOOLEAN'] }
+   }
+ ;
+
+ValueExpression_EDIT
+ : ValueExpression_EDIT LikeRightPart               -> { types: [ 'BOOLEAN' ], suggestFilters: $1.suggestFilters }
+ | ValueExpression_EDIT 'NOT' LikeRightPart         -> { types: [ 'BOOLEAN' ], suggestFilters: $1.suggestFilters }
+ | ValueExpression LikeRightPart_EDIT               -> { types: [ 'BOOLEAN' ] }
+ | ValueExpression 'NOT' LikeRightPart_EDIT         -> { types: [ 'BOOLEAN' ] }
+ | 'CURSOR' LikeRightPart
+   {
+     parser.valueExpressionSuggest(undefined, $2);
+     parser.applyTypeToSuggestions([ 'STRING' ]);
+     $$ = { types: [ 'BOOLEAN' ], typeSet: true };
+   }
+ | 'CURSOR' 'NOT' LikeRightPart
+   {
+     parser.valueExpressionSuggest(undefined, $2 + ' ' + $3);
+     parser.applyTypeToSuggestions([ 'STRING' ]);
+     $$ = { types: [ 'BOOLEAN' ], typeSet: true };
+   }
+ ;
+
+// ------------------  CASE, WHEN, THEN ------------------
+
+ValueExpression
+ : 'CASE' CaseRightPart                  -> $2
+ | 'CASE' ValueExpression CaseRightPart  -> $3
+ ;
+
+ValueExpression_EDIT
+ : 'CASE' CaseRightPart_EDIT                         -> $2
+ | 'CASE' 'CURSOR' EndOrError
+   {
+     parser.valueExpressionSuggest();
+     parser.suggestKeywords(['WHEN']);
+     $$ = { types: [ 'T' ], typeSet: true };
+   }
+ | 'CASE' ValueExpression CaseRightPart_EDIT         -> $3
+ | 'CASE' ValueExpression 'CURSOR' EndOrError
+   {
+     parser.suggestValueExpressionKeywords($2, ['WHEN']);
+     $$ = { types: [ 'T' ], typeSet: true };
+   }
+ | 'CASE' ValueExpression_EDIT CaseRightPart
+    {
+      $$ = $3;
+      $$.suggestFilters = $2.suggestFilters;
+    }
+ | 'CASE' ValueExpression_EDIT EndOrError            -> { types: [ 'T' ], suggestFilters: $2.suggestFilters }
+ | 'CASE' 'CURSOR' CaseRightPart                     -> { types: [ 'T' ] }
+ ;
+
+CaseRightPart
+ : CaseWhenThenList 'END'                         -> parser.findCaseType($1)
+ | CaseWhenThenList 'ELSE' ValueExpression 'END'
+   {
+     $1.caseTypes.push($3);
+     $$ = parser.findCaseType($1);
+   }
+ ;
+
+CaseRightPart_EDIT
+ : CaseWhenThenList_EDIT EndOrError                            -> parser.findCaseType($1)
+ | CaseWhenThenList 'ELSE' ValueExpression 'CURSOR'
+   {
+     parser.suggestValueExpressionKeywords($3, ['END']);
+     $1.caseTypes.push($3);
+     $$ = parser.findCaseType($1);
+   }
+ | CaseWhenThenList_EDIT 'ELSE' ValueExpression EndOrError
+   {
+     $1.caseTypes.push($3);
+     $$ = parser.findCaseType($1);
+   }
+ | CaseWhenThenList_EDIT 'ELSE' EndOrError                      -> parser.findCaseType($1)
+ | CaseWhenThenList 'CURSOR' ValueExpression EndOrError
+   {
+     if ($4.toLowerCase() !== 'end') {
+       parser.suggestValueExpressionKeywords($1, [{ value: 'END', weight: 3 }, { value: 'ELSE', weight: 2 }, { value: 'WHEN', weight: 1 }]);
+     } else {
+       parser.suggestValueExpressionKeywords($1, [{ value: 'ELSE', weight: 2 }, { value: 'WHEN', weight: 1 }]);
+     }
+     $$ = parser.findCaseType($1);
+   }
+ | CaseWhenThenList 'CURSOR' EndOrError
+   {
+     if ($3.toLowerCase() !== 'end') {
+       parser.suggestValueExpressionKeywords($1, [{ value: 'END', weight: 3 }, { value: 'ELSE', weight: 2 }, { value: 'WHEN', weight: 1 }]);
+     } else {
+       parser.suggestValueExpressionKeywords($1, [{ value: 'ELSE', weight: 2 }, { value: 'WHEN', weight: 1 }]);
+     }
+     $$ = parser.findCaseType($1);
+   }
+ | CaseWhenThenList 'ELSE' ValueExpression_EDIT EndOrError
+   {
+     $1.caseTypes.push($3);
+     $$ = parser.findCaseType($1);
+     $$.suggestFilters = $3.suggestFilters
+   }
+ | CaseWhenThenList 'ELSE' 'CURSOR' EndOrError
+   {
+     parser.valueExpressionSuggest();
+     $$ = parser.findCaseType($1);
+   }
+ | 'ELSE' 'CURSOR' EndOrError
+   {
+     parser.valueExpressionSuggest();
+     $$ = { types: [ 'T' ], typeSet: true };
+   }
+ | 'CURSOR' 'ELSE' ValueExpression EndOrError
+   {
+     parser.valueExpressionSuggest();
+     parser.suggestKeywords(['WHEN']);
+     $$ = $3;
+   }
+ | 'CURSOR' 'ELSE' EndOrError
+   {
+     parser.valueExpressionSuggest();
+     parser.suggestKeywords(['WHEN']);
+     $$ = { types: [ 'T' ] };
+   }
+ ;
+
+EndOrError
+ : 'END'
+ | error
+ ;
+
+CaseWhenThenList
+ : CaseWhenThenListPartTwo                   -> { caseTypes: [ $1 ], lastType: $1 }
+ | CaseWhenThenList CaseWhenThenListPartTwo
+   {
+     $1.caseTypes.push($2);
+     $$ = { caseTypes: $1.caseTypes, lastType: $2 };
+   }
+ ;
+
+CaseWhenThenList_EDIT
+ : CaseWhenThenListPartTwo_EDIT
+ | CaseWhenThenList CaseWhenThenListPartTwo_EDIT
+ | CaseWhenThenList CaseWhenThenListPartTwo_EDIT CaseWhenThenList
+ | CaseWhenThenList 'CURSOR' CaseWhenThenList
+   {
+     parser.suggestValueExpressionKeywords($1, ['WHEN']);
+   }
+ | CaseWhenThenListPartTwo_EDIT CaseWhenThenList                   -> $2
+ ;
+
+CaseWhenThenListPartTwo
+ : 'WHEN' ValueExpression 'THEN' ValueExpression  -> $4
+ ;
+
+CaseWhenThenListPartTwo_EDIT
+ : 'WHEN' ValueExpression_EDIT                         -> { caseTypes: [{ types: ['T'] }], suggestFilters: $2.suggestFilters }
+ | 'WHEN' ValueExpression_EDIT 'THEN'                  -> { caseTypes: [{ types: ['T'] }], suggestFilters: $2.suggestFilters }
+ | 'WHEN' ValueExpression_EDIT 'THEN' ValueExpression  -> { caseTypes: [$4], suggestFilters: $2.suggestFilters }
+ | 'WHEN' ValueExpression 'THEN' ValueExpression_EDIT  -> { caseTypes: [$4], suggestFilters: $4.suggestFilters }
+ | 'WHEN' 'THEN' ValueExpression_EDIT                  -> { caseTypes: [$3], suggestFilters: $3.suggestFilters }
+ | 'CURSOR' ValueExpression 'THEN'
+   {
+     parser.suggestKeywords(['WHEN']);
+     $$ = { caseTypes: [{ types: ['T'] }] };
+   }
+ | 'CURSOR' ValueExpression 'THEN' ValueExpression
+   {
+     parser.suggestKeywords(['WHEN']);
+     $$ = { caseTypes: [$4] };
+   }
+ | 'CURSOR' 'THEN'
+   {
+     parser.valueExpressionSuggest();
+     parser.suggestKeywords(['WHEN']);
+     $$ = { caseTypes: [{ types: ['T'] }] };
+   }
+ | 'CURSOR' 'THEN' ValueExpression
+    {
+      parser.valueExpressionSuggest();
+      parser.suggestKeywords(['WHEN']);
+      $$ = { caseTypes: [{ types: ['T'] }] };
+    }
+ | 'WHEN' 'CURSOR'
+   {
+     parser.valueExpressionSuggest();
+     $$ = { caseTypes: [{ types: ['T'] }], suggestFilters: true };
+   }
+ | 'WHEN' 'CURSOR' ValueExpression
+   {
+     parser.valueExpressionSuggest();
+     parser.suggestKeywords(['THEN']);
+     $$ = { caseTypes: [{ types: ['T'] }], suggestFilters: true };
+   }
+ | 'WHEN' 'CURSOR' 'THEN'
+   {
+     parser.valueExpressionSuggest();
+     $$ = { caseTypes: [{ types: ['T'] }], suggestFilters: true };
+   }
+ | 'WHEN' 'CURSOR' 'THEN' ValueExpression
+   {
+     parser.valueExpressionSuggest();
+     $$ = { caseTypes: [$4], suggestFilters: true };
+   }
+ | 'WHEN' ValueExpression 'CURSOR'
+   {
+     parser.suggestValueExpressionKeywords($2, ['THEN']);
+     $$ = { caseTypes: [{ types: ['T'] }] };
+   }
+ | 'WHEN' ValueExpression 'CURSOR' ValueExpression
+   {
+     parser.suggestValueExpressionKeywords($2, ['THEN']);
+     $$ = { caseTypes: [{ types: ['T'] }] };
+   }
+ | 'WHEN' ValueExpression 'THEN' 'CURSOR'
+   {
+     parser.valueExpressionSuggest();
+     $$ = { caseTypes: [{ types: ['T'] }] };
+   }
+ | 'WHEN' ValueExpression 'THEN' 'CURSOR' ValueExpression
+   {
+     parser.valueExpressionSuggest();
+     $$ = { caseTypes: [{ types: ['T'] }] };
+   }
+ | 'WHEN' 'THEN' 'CURSOR' ValueExpression
+   {
+     parser.valueExpressionSuggest();
+     $$ = { caseTypes: [{ types: ['T'] }] };
+   }
+ | 'WHEN' 'THEN' 'CURSOR'
+   {
+     parser.valueExpressionSuggest();
+     $$ = { caseTypes: [{ types: ['T'] }] };
+   }
+ ;

+ 19 - 0
desktop/core/src/desktop/js/parse/jison/sql/ksql/syntax_footer.jison

@@ -0,0 +1,19 @@
+// 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.
+
+%%
+
+SqlParseSupport.initSyntaxParser(parser);

+ 28 - 0
desktop/core/src/desktop/js/parse/jison/sql/ksql/syntax_header.jison

@@ -0,0 +1,28 @@
+// 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.
+
+%left 'AND' 'OR'
+%left 'BETWEEN'
+%left 'NOT' '!' '~'
+%left '=' '<' '>' 'COMPARISON_OPERATOR'
+%left '-' '*' 'ARITHMETIC_OPERATOR'
+
+%left ';' ','
+%nonassoc 'IN' 'IS' 'LIKE' 'RLIKE' 'REGEXP' 'EXISTS' NEGATION
+
+%start SqlSyntax
+
+%%

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


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


+ 376 - 0
desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParserSpec.js

@@ -0,0 +1,376 @@
+// 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 SqlTestUtils from 'parse/spec/sqlTestUtils';
+import ksqlAutocompleteParser from '../ksqlAutocompleteParser';
+
+describe('ksqlAutocompleteParser.js', () => {
+  beforeAll(() => {
+    ksqlAutocompleteParser.yy.parseError = function(msg) {
+      throw Error(msg);
+    };
+    jasmine.addMatchers(SqlTestUtils.testDefinitionMatcher);
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = false;
+
+    expect(
+      ksqlAutocompleteParser.parseSql(
+        testDefinition.beforeCursor,
+        testDefinition.afterCursor,
+        debug
+      )
+    ).toEqualDefinition(testDefinition);
+  };
+
+  it('should suggest keywords for ";;|"', () => {
+    assertAutoComplete({
+      beforeCursor: ';;',
+      afterCursor: '',
+      containsKeywords: ['SELECT', 'WITH'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest keywords for ";|;"', () => {
+    assertAutoComplete({
+      beforeCursor: ';',
+      afterCursor: ';',
+      containsKeywords: ['SELECT'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest keywords for "|;;;;', () => {
+    assertAutoComplete({
+      beforeCursor: '',
+      afterCursor: ';;;;',
+      containsKeywords: ['SELECT'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest keywords for "foo|bar"', () => {
+    assertAutoComplete({
+      beforeCursor: 'foo',
+      afterCursor: 'bar',
+      containsKeywords: ['SELECT'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  describe('Error Handling', () => {
+    it('should suggest keywords for "bla; |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'bla; ',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "bla bla bla;bla; |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'bla bla bla;bla; ',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "Åäö; |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'Åäö; ',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "bla bla bla;bla;\\n|;bladiblaa blaa"', () => {
+      assertAutoComplete({
+        beforeCursor: 'bla bla bla;bla;\n',
+        afterCursor: ';bladiblaa blaa',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "FROM; |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'FROM; ',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "INTO USE; |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'INTO USE; ',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "INTO SELECT; OR FROM FROM; |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'INTO SELECT; OR FROM FROM;',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "INTO SELECT; OR FROM FROM; |;BLAAA; AND;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'INTO SELECT; OR FROM FROM;',
+        afterCursor: ';BLAAA; AND;',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "INTO bla bla;AND booo; |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'INTO bla bla;AND booo;',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "|; SELECT LIMIT 10"', () => {
+      assertAutoComplete({
+        beforeCursor: '',
+        afterCursor: '; SELECT LIMIT 10',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "| * FROM boo; SELECT LIMIT 10"', () => {
+      assertAutoComplete({
+        beforeCursor: '',
+        afterCursor: ' * FROM boo; SELECT LIMIT 10',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "bla| * FROM boo; SELECT LIMIT 10"', () => {
+      assertAutoComplete({
+        beforeCursor: 'bla',
+        afterCursor: ' * FROM boo; SELECT LIMIT 10',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+  });
+
+  describe('SET', () => {
+    it('should handle "set bla.bla="ble";|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'set bla.bla="ble";',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: true
+        }
+      });
+    });
+
+    it('should handle "set bla.bla=\'ble\';|"', () => {
+      assertAutoComplete({
+        beforeCursor: "set bla.bla='ble';",
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: true
+        }
+      });
+    });
+
+    it('should handle "set mem_limit=64g;|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'set mem_limit=64g;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: true
+        }
+      });
+    });
+
+    it('should handle "set DISABLE_UNSAFE_SPILLS=true;|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'set DISABLE_UNSAFE_SPILLS=true;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: true
+        }
+      });
+    });
+
+    it('should handle "set RESERVATION_REQUEST_TIMEOUT=900000;|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'set RESERVATION_REQUEST_TIMEOUT=900000;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: true
+        }
+      });
+    });
+  });
+
+  describe('partial removal', () => {
+    it('should identify part lengths', () => {
+      const limitChars = [
+        ' ',
+        '\n',
+        '\t',
+        '&',
+        '~',
+        '%',
+        '!',
+        '.',
+        ',',
+        '+',
+        '-',
+        '*',
+        '/',
+        '=',
+        '<',
+        '>',
+        ')',
+        '[',
+        ']',
+        ';'
+      ];
+
+      expect(ksqlAutocompleteParser.identifyPartials('', '')).toEqual({ left: 0, right: 0 });
+      expect(ksqlAutocompleteParser.identifyPartials('foo', '')).toEqual({ left: 3, right: 0 });
+      expect(ksqlAutocompleteParser.identifyPartials(' foo', '')).toEqual({ left: 3, right: 0 });
+      expect(ksqlAutocompleteParser.identifyPartials('asdf 1234', '')).toEqual({
+        left: 4,
+        right: 0
+      });
+
+      expect(ksqlAutocompleteParser.identifyPartials('foo', 'bar')).toEqual({
+        left: 3,
+        right: 3
+      });
+
+      expect(ksqlAutocompleteParser.identifyPartials('fo', 'o()')).toEqual({
+        left: 2,
+        right: 3
+      });
+
+      expect(ksqlAutocompleteParser.identifyPartials('fo', 'o(')).toEqual({ left: 2, right: 2 });
+      expect(ksqlAutocompleteParser.identifyPartials('fo', 'o(bla bla)')).toEqual({
+        left: 2,
+        right: 10
+      });
+
+      expect(ksqlAutocompleteParser.identifyPartials('foo ', '')).toEqual({ left: 0, right: 0 });
+      expect(ksqlAutocompleteParser.identifyPartials("foo '", "'")).toEqual({
+        left: 0,
+        right: 0
+      });
+
+      expect(ksqlAutocompleteParser.identifyPartials('foo "', '"')).toEqual({
+        left: 0,
+        right: 0
+      });
+      limitChars.forEach(char => {
+        expect(ksqlAutocompleteParser.identifyPartials('bar foo' + char, '')).toEqual({
+          left: 0,
+          right: 0
+        });
+
+        expect(ksqlAutocompleteParser.identifyPartials('bar foo' + char + 'foofoo', '')).toEqual(
+          {
+            left: 6,
+            right: 0
+          }
+        );
+
+        expect(
+          ksqlAutocompleteParser.identifyPartials('bar foo' + char + 'foofoo ', '')
+        ).toEqual({
+          left: 0,
+          right: 0
+        });
+
+        expect(ksqlAutocompleteParser.identifyPartials('', char + 'foo bar')).toEqual({
+          left: 0,
+          right: 0
+        });
+
+        expect(ksqlAutocompleteParser.identifyPartials('', 'foofoo' + char)).toEqual({
+          left: 0,
+          right: 6
+        });
+
+        expect(ksqlAutocompleteParser.identifyPartials('', ' foofoo' + char)).toEqual({
+          left: 0,
+          right: 0
+        });
+      });
+    });
+  });
+});

+ 157 - 0
desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParser_Alter_Spec.js

@@ -0,0 +1,157 @@
+// 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 SqlTestUtils from 'parse/spec/sqlTestUtils';
+import ksqlAutocompleteParser from '../ksqlAutocompleteParser';
+
+describe('ksqlAutocompleteParser.js ALTER statements', () => {
+  beforeAll(() => {
+    ksqlAutocompleteParser.yy.parseError = function(msg) {
+      throw Error(msg);
+    };
+    jasmine.addMatchers(SqlTestUtils.testDefinitionMatcher);
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = false;
+
+    expect(
+      ksqlAutocompleteParser.parseSql(
+        testDefinition.beforeCursor,
+        testDefinition.afterCursor,
+        debug
+      )
+    ).toEqualDefinition(testDefinition);
+  };
+
+  describe('ALTER TABLE', () => {
+    it('should suggest keywords for "ALTER |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'ALTER ',
+        afterCursor: '',
+        containsKeywords: ['TABLE'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest tables for "ALTER TABLE |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'ALTER TABLE ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { onlyTables: true },
+          suggestDatabases: { appendDot: true }
+        }
+      });
+    });
+
+    it('should suggest tables for "ALTER TABLE foo.|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'ALTER TABLE foo.',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { identifierChain: [{ name: 'foo' }], onlyTables: true }
+        }
+      });
+    });
+  });
+
+  describe('ALTER VIEW', () => {
+    it('should handle "ALTER VIEW baa.boo AS SELECT * FROM bla;|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'ALTER VIEW baa.boo AS SELECT * FROM bla;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "ALTER |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'ALTER ',
+        afterCursor: '',
+        containsKeywords: ['VIEW'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest views for "ALTER VIEW |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'ALTER VIEW ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { onlyViews: true },
+          suggestDatabases: { appendDot: true }
+        }
+      });
+    });
+
+    it('should suggest views for "ALTER VIEW boo.|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'ALTER VIEW boo.',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { identifierChain: [{ name: 'boo' }], onlyViews: true }
+        }
+      });
+    });
+
+    it('should suggest keywords for "ALTER VIEW boo |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'ALTER VIEW boo ',
+        afterCursor: '',
+        containsKeywords: ['AS'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "ALTER VIEW baa.boo AS |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'ALTER VIEW baa.boo AS ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['SELECT']
+        }
+      });
+    });
+
+    it('should suggest databases for "ALTER VIEW baa.boo AS SELECT * FROM |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'ALTER VIEW baa.boo AS SELECT * FROM ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: {},
+          suggestDatabases: { appendDot: true }
+        }
+      });
+    });
+  });
+});

+ 277 - 0
desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParser_Create_Spec.js

@@ -0,0 +1,277 @@
+// 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 SqlTestUtils from 'parse/spec/sqlTestUtils';
+import ksqlAutocompleteParser from '../ksqlAutocompleteParser';
+
+describe('ksqlAutocompleteParser.js CREATE statements', () => {
+  beforeAll(() => {
+    ksqlAutocompleteParser.yy.parseError = function(msg) {
+      throw Error(msg);
+    };
+    jasmine.addMatchers(SqlTestUtils.testDefinitionMatcher);
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = false;
+
+    expect(
+      ksqlAutocompleteParser.parseSql(
+        testDefinition.beforeCursor,
+        testDefinition.afterCursor,
+        debug
+      )
+    ).toEqualDefinition(testDefinition);
+  };
+
+  it('should suggest keywords for "|"', () => {
+    assertAutoComplete({
+      beforeCursor: '',
+      afterCursor: '',
+      containsKeywords: ['CREATE'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest keywords for "CREATE |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'CREATE ',
+      afterCursor: '',
+      containsKeywords: ['DATABASE', 'ROLE', 'SCHEMA', 'TABLE', 'VIEW'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  describe('CREATE DATABASE', () => {
+    it('should suggest keywords for "CREATE DATABASE |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE DATABASE ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['IF NOT EXISTS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE DATABASE IF |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE DATABASE IF ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['NOT EXISTS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE SCHEMA |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE SCHEMA ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['IF NOT EXISTS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE DATABASE | bla;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE DATABASE ',
+        afterCursor: ' bla;',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['IF NOT EXISTS']
+        }
+      });
+    });
+  });
+
+  describe('CREATE ROLE', () => {
+    it('should handle "CREATE ROLE boo; |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE ROLE boo; ',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+  });
+
+  describe('CREATE TABLE', () => {
+    it('should suggest keywords for "CREATE TABLE |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE TABLE ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['IF NOT EXISTS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE TABLE IF |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE TABLE IF ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['NOT EXISTS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE TABLE IF NOT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE TABLE IF NOT ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['EXISTS']
+        }
+      });
+    });
+
+    it('should handle for "CREATE TABLE foo (id INT);|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE TABLE foo (id INT);',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE TABLE foo (id |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE TABLE foo (id ',
+        afterCursor: '',
+        containsKeywords: ['BOOLEAN'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE TABLE foo (id INT, `some` FLOAT, bar |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE TABLE foo (id INT, `some` FLOAT, bar ',
+        afterCursor: '',
+        containsKeywords: ['BOOLEAN'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+  });
+
+  describe('CREATE VIEW', () => {
+    it('should handle "CREATE VIEW foo AS SELECT a, | FROM tableOne"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE VIEW foo AS SELECT a, ',
+        afterCursor: ' FROM tableOne',
+        hasLocations: true,
+        containsKeywords: ['*', 'CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'tableOne' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'tableOne' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE VIEW |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE VIEW ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['IF NOT EXISTS'],
+          suggestDatabases: { appendDot: true }
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE VIEW | boo AS select * from baa;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE VIEW ',
+        afterCursor: ' boo AS SELECT * FROM baa;',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['IF NOT EXISTS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE VIEW IF |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE VIEW IF ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['NOT EXISTS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE VIEW IF NOT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE VIEW IF NOT ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['EXISTS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE VIEW boo AS |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE VIEW boo AS ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['SELECT']
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE VIEW IF NOT EXISTS boo AS |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE VIEW IF NOT EXISTS boo AS ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['SELECT']
+        }
+      });
+    });
+  });
+});

+ 290 - 0
desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParser_Drop_Spec.js

@@ -0,0 +1,290 @@
+// 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 SqlTestUtils from 'parse/spec/sqlTestUtils';
+import ksqlAutocompleteParser from '../ksqlAutocompleteParser';
+
+describe('ksqlAutocompleteParser.js DROP statements', () => {
+  beforeAll(() => {
+    ksqlAutocompleteParser.yy.parseError = function(msg) {
+      throw Error(msg);
+    };
+    jasmine.addMatchers(SqlTestUtils.testDefinitionMatcher);
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = false;
+
+    expect(
+      ksqlAutocompleteParser.parseSql(
+        testDefinition.beforeCursor,
+        testDefinition.afterCursor,
+        debug
+      )
+    ).toEqualDefinition(testDefinition);
+  };
+
+  it('should suggest keywords for "DROP |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'DROP ',
+      afterCursor: '',
+      containsKeywords: ['DATABASE', 'ROLE', 'SCHEMA', 'TABLE', 'VIEW'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  describe('DROP DATABASE', () => {
+    it('should suggest databases for "DROP DATABASE |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP DATABASE ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestDatabases: {},
+          suggestKeywords: ['IF EXISTS']
+        }
+      });
+    });
+
+    it('should suggest databases for "DROP SCHEMA |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP SCHEMA ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestDatabases: {},
+          suggestKeywords: ['IF EXISTS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "DROP DATABASE IF |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP DATABASE IF ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['EXISTS']
+        }
+      });
+    });
+
+    it('should suggest databases for "DROP DATABASE IF EXISTS |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP DATABASE IF EXISTS ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestDatabases: {}
+        }
+      });
+    });
+
+    it('should suggest keywords for "DROP DATABASE foo |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP DATABASE foo ',
+        afterCursor: '',
+        containsKeywords: ['CASCADE'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+  });
+
+  describe('DROP ROLE', () => {
+    it('should handle "DROP ROLE boo;|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP ROLE boo;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+  });
+
+  describe('DROP TABLE', () => {
+    it('should handle "DROP TABLE db.tbl PURGE;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP TABLE db.tbl PURGE;',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest tables for "DROP TABLE |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP TABLE ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { onlyTables: true },
+          suggestKeywords: ['IF EXISTS'],
+          suggestDatabases: {
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest tables for "DROP TABLE db.|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP TABLE db.',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { identifierChain: [{ name: 'db' }], onlyTables: true }
+        }
+      });
+    });
+
+    it('should suggest keywords for "DROP TABLE IF |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP TABLE IF ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['EXISTS']
+        }
+      });
+    });
+
+    it('should suggest tables for "DROP TABLE IF EXISTS |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP TABLE IF EXISTS ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { onlyTables: true },
+          suggestDatabases: {
+            appendDot: true
+          }
+        }
+      });
+    });
+  });
+
+  describe('DROP VIEW', () => {
+    it('should handle "DROP VIEW boo;|', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP VIEW boo;',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should handle "DROP VIEW IF EXISTS baa.boo;|', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP VIEW IF EXISTS baa.boo;',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest views for "DROP VIEW |', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP VIEW ',
+        afterCursor: '',
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { onlyViews: true },
+          suggestDatabases: { appendDot: true },
+          suggestKeywords: ['IF EXISTS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "DROP VIEW IF |', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP VIEW IF ',
+        afterCursor: '',
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['EXISTS']
+        }
+      });
+    });
+
+    it('should suggest views for "DROP VIEW boo.|', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP VIEW boo.',
+        afterCursor: '',
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { identifierChain: [{ name: 'boo' }], onlyViews: true }
+        }
+      });
+    });
+  });
+
+  describe('TRUNCATE TABLE', () => {
+    it('should handle "TRUNCATE TABLE baa.boo;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'TRUNCATE TABLE baa.boo;',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "TRUNCATE |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'truncate ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: true,
+          suggestKeywords: ['TABLE']
+        }
+      });
+    });
+
+    it('should suggest tables for "TRUNCATE TABLE |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'TRUNCATE TABLE ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: {},
+          suggestDatabases: { appendDot: true },
+          suggestKeywords: ['IF EXISTS']
+        }
+      });
+    });
+  });
+});

+ 138 - 0
desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParser_Error_Spec.js

@@ -0,0 +1,138 @@
+// 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 SqlTestUtils from 'parse/spec/sqlTestUtils';
+import ksqlAutocompleteParser from '../ksqlAutocompleteParser';
+
+describe('ksqlAutocompleteParser.js Error statements', () => {
+  beforeAll(() => {
+    ksqlAutocompleteParser.yy.parseError = function(msg) {
+      throw Error(msg);
+    };
+    jasmine.addMatchers(SqlTestUtils.testDefinitionMatcher);
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = false;
+
+    expect(
+      ksqlAutocompleteParser.parseSql(
+        testDefinition.beforeCursor,
+        testDefinition.afterCursor,
+        debug
+      )
+    ).toEqualDefinition(testDefinition);
+  };
+
+  it('should suggest columns for "SELECT BAABO BOOAA BLARGH, | FROM testTable"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT BAABO BOOAA BLARGH, ',
+      afterCursor: ' FROM testTable',
+      expectedResult: {
+        lowerCase: false,
+        suggestFunctions: {},
+        suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+        suggestAnalyticFunctions: true,
+        suggestColumns: { tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+      }
+    });
+  });
+
+  it('should suggest columns for "SELECT foo, bar, SELECT, | FROM testTable"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT foo, bar, SELECT, ',
+      afterCursor: ' FROM testTable',
+      expectedResult: {
+        lowerCase: false,
+        suggestFunctions: {},
+        suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+        suggestAnalyticFunctions: true,
+        suggestColumns: { tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+      }
+    });
+  });
+
+  it('should suggest columns for "SELECT foo, baa baa baa baa, SELECT, | FROM testTable"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT foo, baa baa baa baa, SELECT, ',
+      afterCursor: ' FROM testTable',
+      expectedResult: {
+        lowerCase: false,
+        suggestFunctions: {},
+        suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+        suggestAnalyticFunctions: true,
+        suggestColumns: { tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+      }
+    });
+  });
+
+  it('should suggest columns for "SELECT * FROM testTable WHERE baa baaa booo |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM testTable WHERE baa baaa boo',
+      afterCursor: '',
+      containsKeywords: ['GROUP BY', 'ORDER BY'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest columns for "SELECT * FROM testTable WHERE baa baaa booo GROUP |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM testTable WHERE baa baaa boo GROUP ',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestKeywords: ['BY'],
+        suggestGroupBys: { prefix: 'BY', tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+      }
+    });
+  });
+
+  it('should suggest columns for "SELECT * FROM testTable WHERE baa baaa booo ORDER |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM testTable WHERE baa baaa boo ORDER ',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestKeywords: ['BY'],
+        suggestOrderBys: { prefix: 'BY', tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+      }
+    });
+  });
+
+  it('should suggest columns for "SELECT * FROM testTable ORDER BY bla bla bla boo |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM testTable ORDER BY bla bla bla boo ',
+      afterCursor: '',
+      containsKeywords: ['LIMIT', 'UNION'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest columns for "SELECT * FROM testTable GROUP BY boo hoo hoo ORDER BY bla bla bla boo |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM testTable ORDER BY bla bla bla boo ',
+      afterCursor: '',
+      containsKeywords: ['LIMIT', 'UNION'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+});

+ 139 - 0
desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParser_Insert_Spec.js

@@ -0,0 +1,139 @@
+// 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 SqlTestUtils from 'parse/spec/sqlTestUtils';
+import ksqlAutocompleteParser from '../ksqlAutocompleteParser';
+
+describe('ksqlAutocompleteParser.js INSERT statements', () => {
+  beforeAll(() => {
+    ksqlAutocompleteParser.yy.parseError = function(msg) {
+      throw Error(msg);
+    };
+    jasmine.addMatchers(SqlTestUtils.testDefinitionMatcher);
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = false;
+
+    expect(
+      ksqlAutocompleteParser.parseSql(
+        testDefinition.beforeCursor,
+        testDefinition.afterCursor,
+        debug
+      )
+    ).toEqualDefinition(testDefinition);
+  };
+
+  describe('INSERT', () => {
+    it('should handle "INSERT INTO bla.boo VALUES (1, 2, \'a\', 3); |"', () => {
+      assertAutoComplete({
+        beforeCursor: "INSERT INTO bla.boo VALUES (1, 2, 'a', 3); ",
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "|"', () => {
+      assertAutoComplete({
+        beforeCursor: '',
+        afterCursor: '',
+        containsKeywords: ['INSERT'],
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "INSERT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'INSERT ',
+        afterCursor: '',
+        containsKeywords: ['INTO'],
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest tables for "INSERT INTO |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'INSERT INTO ',
+        afterCursor: '',
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: {},
+          suggestDatabases: { appendDot: true },
+          suggestKeywords: ['TABLE']
+        }
+      });
+    });
+
+    it('should suggest tables for "INSERT INTO baa.|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'INSERT INTO baa.',
+        afterCursor: '',
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { identifierChain: [{ name: 'baa' }] }
+        }
+      });
+    });
+
+    it('should suggest tables for "INSERT INTO TABLE baa.|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'INSERT INTO TABLE baa.',
+        afterCursor: '',
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { identifierChain: [{ name: 'baa' }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "INSERT INTO baa |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'INSERT INTO baa ',
+        afterCursor: '',
+        containsKeywords: ['VALUES'],
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "INSERT INTO TABLE baa |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'INSERT INTO TABLE baa ',
+        afterCursor: '',
+        containsKeywords: ['VALUES'],
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+  });
+});

+ 423 - 0
desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParser_Locations_Spec.js

@@ -0,0 +1,423 @@
+// 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 SqlTestUtils from 'parse/spec/sqlTestUtils';
+import ksqlAutocompleteParser from '../ksqlAutocompleteParser';
+
+// prettier-ignore-start
+describe('ksqlAutocompleteParser.js locations', () => {
+  beforeAll(() => {
+    ksqlAutocompleteParser.yy.parseError = function(msg) {
+      throw Error(msg);
+    };
+    jasmine.addMatchers(SqlTestUtils.testDefinitionMatcher);
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = true;
+    expect(
+      ksqlAutocompleteParser.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: '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: '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: '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

+ 6174 - 0
desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParser_Select_Spec.js

@@ -0,0 +1,6174 @@
+// 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 SqlTestUtils from 'parse/spec/sqlTestUtils';
+import ksqlAutocompleteParser from '../ksqlAutocompleteParser';
+
+describe('ksqlAutocompleteParser.js SELECT statements', () => {
+  beforeAll(() => {
+    ksqlAutocompleteParser.yy.parseError = function(msg) {
+      throw Error(msg);
+    };
+    jasmine.addMatchers(SqlTestUtils.testDefinitionMatcher);
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = false;
+
+    expect(
+      ksqlAutocompleteParser.parseSql(
+        testDefinition.beforeCursor,
+        testDefinition.afterCursor,
+        debug
+      )
+    ).toEqualDefinition(testDefinition);
+  };
+
+  it('should suggest keywords for "|"', () => {
+    assertAutoComplete({
+      beforeCursor: '',
+      afterCursor: '',
+      containsKeywords: ['SELECT'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest tables and databases for "SELECT * |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * ',
+      afterCursor: '',
+      containsKeywords: ['FROM'],
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: {
+          prependFrom: true
+        },
+        suggestDatabases: {
+          prependFrom: true,
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should suggest tables and databases for "SELECT *\\r\\n |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT *\r\n',
+      afterCursor: '',
+      containsKeywords: ['FROM'],
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: {
+          prependFrom: true
+        },
+        suggestDatabases: {
+          prependFrom: true,
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should not suggest anything for "SELECT u.|"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT u.',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest keywords for "SELECT foo, bar |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT foo, bar ',
+      afterCursor: '',
+      containsKeywords: ['AS', '+', 'FROM', 'DIV'],
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: {
+          prependFrom: true
+        },
+        suggestDatabases: {
+          prependFrom: true,
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should suggest keywords for "SELECT foo AS a, bar |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT foo AS a, bar ',
+      afterCursor: '',
+      containsKeywords: ['AS', '+'],
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: {
+          prependFrom: true
+        },
+        suggestDatabases: {
+          prependFrom: true,
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should suggest keywords for "SELECT * FROM testTableA tta, testTableB |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM testTableA tta, testTableB ',
+      afterCursor: '',
+      expectedResult: {
+        suggestJoins: {
+          prependJoin: true,
+          tables: [{ identifierChain: [{ name: 'testTableB' }] }]
+        },
+        suggestFilters: {
+          prefix: 'WHERE',
+          tables: [
+            { identifierChain: [{ name: 'testTableA' }], alias: 'tta' },
+            { identifierChain: [{ name: 'testTableB' }] }
+          ]
+        },
+        suggestGroupBys: {
+          prefix: 'GROUP BY',
+          tables: [
+            { identifierChain: [{ name: 'testTableA' }], alias: 'tta' },
+            { identifierChain: [{ name: 'testTableB' }] }
+          ]
+        },
+        suggestOrderBys: {
+          prefix: 'ORDER BY',
+          tables: [
+            { identifierChain: [{ name: 'testTableA' }], alias: 'tta' },
+            { identifierChain: [{ name: 'testTableB' }] }
+          ]
+        },
+        suggestKeywords: [
+          'AS',
+          'WHERE',
+          'GROUP BY',
+          'HAVING',
+          'ORDER BY',
+          'LIMIT',
+          'UNION',
+          'FULL JOIN',
+          'FULL OUTER JOIN',
+          'INNER JOIN',
+          'JOIN',
+          'LEFT JOIN',
+          'LEFT OUTER JOIN',
+          'RIGHT JOIN',
+          'RIGHT OUTER JOIN'
+        ],
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest databases or tables for "SELECT * fr|"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * fr',
+      afterCursor: '',
+      containsKeywords: ['FROM'],
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: {
+          prependFrom: true
+        },
+        suggestDatabases: {
+          prependFrom: true,
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should suggest databases or tables for "SELECT * FROM |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM ',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: {},
+        suggestDatabases: {
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should suggest databases or tables for "SELECT * FROM tes|"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM tes',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: {},
+        suggestDatabases: {
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should suggest databases or tables for "SELECT * FROM `tes|"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM `tes',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: {},
+        suggestDatabases: {
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should suggest tables for "SELECT * FROM database_two.|"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM database_two.',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: { identifierChain: [{ name: 'database_two' }] }
+      }
+    });
+  });
+
+  it('should suggest tables for "SELECT * FROM `database_two`.|"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM `database_two`.',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: { identifierChain: [{ name: 'database_two' }] }
+      }
+    });
+  });
+
+  it('should suggest tables for "SELECT * FROM 33abc.|"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM 33abc.',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: { identifierChain: [{ name: '33abc' }] }
+      }
+    });
+  });
+
+  it('should suggest tables for "SELECT * FROM `database_two`.`bla |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM `database_two`.`bla ',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: { identifierChain: [{ name: 'database_two' }] }
+      }
+    });
+  });
+
+  describe('Complete Statements', () => {
+    it('should handle "SELECT 4 / 2; |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT 4 / 2; ',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should handle "SELECT 4 DIV 2; |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT 4 DIV 2; ',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it("should handle \"SELECT bla NOT RLIKE 'ble', ble NOT REGEXP 'b' FROM tbl; |\"", () => {
+      assertAutoComplete({
+        beforeCursor: "SELECT bla NOT RLIKE 'ble', ble NOT REGEXP 'b' FROM tbl; ",
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should handle "SELECT * FROM tbl limit ${limit=20}; |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM tbl limit ${limit=20}; ',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest columns "SELECT IF(baa, boo, bee) AS b, | FROM testTable;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT IF(baa, boo, bee) AS b, ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['*', 'CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestAnalyticFunctions: true,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns "SELECT IF(baa > 2, boo, bee) AS b, | FROM testTable;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT IF(baa > 2, boo, bee) AS b, ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['*', 'CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestAnalyticFunctions: true,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+  });
+
+  describe('Select List Completion', () => {
+    it('should handle "select count(*), tst.count, avg (id), avg from autocomp_test tst;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select count(*), tst.count, avg (id), avg from autocomp_test tst;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 65 }
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 42 }
+            },
+            {
+              type: 'function',
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 12 },
+              function: 'count'
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 18, last_column: 21 },
+              identifierChain: [{ name: 'autocomp_test' }]
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 22, last_column: 27 },
+              identifierChain: [{ name: 'count' }],
+              tables: [{ identifierChain: [{ name: 'autocomp_test' }], alias: 'tst' }],
+              qualified: true
+            },
+            {
+              type: 'function',
+              location: { first_line: 1, last_line: 1, first_column: 29, last_column: 32 },
+              function: 'avg'
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 34, last_column: 36 },
+              identifierChain: [{ name: 'id' }],
+              tables: [{ identifierChain: [{ name: 'autocomp_test' }], alias: 'tst' }],
+              qualified: false
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 39, last_column: 42 },
+              identifierChain: [{ name: 'avg' }],
+              tables: [{ identifierChain: [{ name: 'autocomp_test' }], alias: 'tst' }],
+              qualified: false
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 48, last_column: 61 },
+              identifierChain: [{ name: 'autocomp_test' }]
+            },
+            {
+              type: 'alias',
+              source: 'table',
+              alias: 'tst',
+              location: { first_line: 1, last_line: 1, first_column: 62, last_column: 65 },
+              identifierChain: [{ name: 'autocomp_test' }]
+            },
+            {
+              type: 'whereClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 65, last_column: 65 }
+            },
+            {
+              type: 'limitClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 65, last_column: 65 }
+            }
+          ],
+          lowerCase: true
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: '',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT |;\n\nSELECT * FROM foo;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ';\n\nSELECT * FROM foo;',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT * FROM foo;\n\nSELECT |;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo;\n\nSELECT ',
+        afterCursor: ';',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT |;\n\nSELECT * FROM foo boo;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ';\n\nSELECT * FROM foo boo;',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT * FROM foo boo;\n\nSELECT |;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo boo;\n\nSELECT ',
+        afterCursor: ';',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest lowerCase for "select |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select ',
+        afterCursor: '',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: true,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT ALL |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ALL ',
+        afterCursor: '',
+        containsKeywords: ['*', 'CASE'],
+        doesNotContainKeywords: ['ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT DISTINCT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT DISTINCT ',
+        afterCursor: '',
+        containsKeywords: ['*'],
+        doesNotContainKeywords: ['ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT DISTINCT | a, b, c FROM tbl"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT DISTINCT ',
+        afterCursor: ' a, b, c FROM tbl',
+        containsKeywords: ['*'],
+        doesNotContainKeywords: ['ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'tbl' }] }] },
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 34 }
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 15, last_column: 25 }
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 18, last_column: 19 },
+              identifierChain: [{ name: 'a' }],
+              tables: [{ identifierChain: [{ name: 'tbl' }] }],
+              qualified: false
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 21, last_column: 22 },
+              identifierChain: [{ name: 'b' }],
+              tables: [{ identifierChain: [{ name: 'tbl' }] }],
+              qualified: false
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 24, last_column: 25 },
+              identifierChain: [{ name: 'c' }],
+              tables: [{ identifierChain: [{ name: 'tbl' }] }],
+              qualified: false
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 31, last_column: 34 },
+              identifierChain: [{ name: 'tbl' }]
+            },
+            {
+              type: 'whereClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 34, last_column: 34 }
+            },
+            {
+              type: 'limitClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 34, last_column: 34 }
+            }
+          ]
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT | FROM tableA;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' FROM tableA;',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'tableA' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'tableA' }] }] }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT | AS boo FROM tableA;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' AS boo FROM tableA;',
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'tableA' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'tableA' }] }] }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT | boo FROM tableA;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' boo FROM tableA;',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'tableA' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'tableA' }] }] }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT bla| AS boo FROM tableA;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT bla',
+        afterCursor: ' AS boo FROM tableA;',
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'tableA' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'tableA' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT | FROM testWHERE"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' FROM testWHERE',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testWHERE' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testWHERE' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT (bl|a AND boo FROM testWHERE"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT (bl',
+        afterCursor: ' AND boo FROM testWHERE',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testWHERE' }] }]
+          }
+        }
+      });
+    });
+
+    // TODO: Parser can't handle multiple errors in a row, in this case 2 missing ')')
+    xit('should suggest columns for "SELECT ((| FROM testWHERE"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ((',
+        afterCursor: ' FROM testWHERE',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testWHERE' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT (bla| AND boo FROM testWHERE"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT (bla',
+        afterCursor: ' AND boo FROM testWHERE',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testWHERE' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT | FROM testON"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' FROM testON',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testON' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'testON' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT | FROM transactions"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' FROM transactions',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'transactions' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'transactions' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest aliases for "SELECT | FROM testTableA tta, testTableB"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' FROM testTableA tta, testTableB',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: {
+            tables: [
+              { identifierChain: [{ name: 'testTableA' }], alias: 'tta' },
+              { identifierChain: [{ name: 'testTableB' }] }
+            ]
+          },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [
+              { identifierChain: [{ name: 'testTableA' }], alias: 'tta' },
+              { identifierChain: [{ name: 'testTableB' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'tta.', type: 'alias' },
+            { name: 'testTableB.', type: 'table' }
+          ]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT TTA.| FROM testTableA tta"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT TTA.',
+        afterCursor: ' FROM testTableA tta',
+        containsKeywords: ['*'],
+        expectedResult: {
+          lowerCase: false,
+          // TODO: add alias on table in suggestColumns (needs support in sqlAutocomplete3.js)
+          // Case is: select cu.| from customers
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTableA' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT tta.| FROM testTableA TTA"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT tta.',
+        afterCursor: ' FROM testTableA TTA',
+        containsKeywords: ['*'],
+        expectedResult: {
+          lowerCase: false,
+          // TODO: add alias on table in suggestColumns (needs support in sqlAutocomplete3.js)
+          // Case is: select cu.| from customers
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTableA' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT | FROM db.tbl1, db.tbl2"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' FROM db.tbl1, db.tbl2',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: {
+            tables: [
+              { identifierChain: [{ name: 'db' }, { name: 'tbl1' }] },
+              { identifierChain: [{ name: 'db' }, { name: 'tbl2' }] }
+            ]
+          },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [
+              { identifierChain: [{ name: 'db' }, { name: 'tbl1' }] },
+              { identifierChain: [{ name: 'db' }, { name: 'tbl2' }] }
+            ]
+          },
+          suggestIdentifiers: [{ name: 'tbl1.', type: 'table' }, { name: 'tbl2.', type: 'table' }]
+        }
+      });
+    });
+
+    it('should suggest columns for "select | from database_two.testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select ',
+        afterCursor: ' from database_two.testTable',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: true,
+          suggestAggregateFunctions: {
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable' }] }]
+          },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "select | from `database one`.`test table`"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select ',
+        afterCursor: ' from `database one`.`test table`',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: true,
+          suggestAggregateFunctions: {
+            tables: [{ identifierChain: [{ name: 'database one' }, { name: 'test table' }] }]
+          },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'database one' }, { name: 'test table' }] }]
+          },
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 41 }
+            },
+            {
+              type: 'selectList',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 7, last_column: 7 }
+            },
+            {
+              type: 'database',
+              location: { first_line: 1, last_line: 1, first_column: 14, last_column: 28 },
+              identifierChain: [{ name: 'database one' }]
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 29, last_column: 41 },
+              identifierChain: [{ name: 'database one' }, { name: 'test table' }]
+            },
+            {
+              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 suggest columns for "SELECT a, | FROM tableA;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, ',
+        afterCursor: ' FROM tableA;',
+        containsKeywords: ['*', 'CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'tableA' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'tableA' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a,| FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a,',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['*', 'CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT *, | FROM tableA;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT *, ',
+        afterCursor: ' FROM tableA;',
+        containsKeywords: ['*', 'CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'tableA' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'tableA' }] }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT a | FROM tableA;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a ',
+        afterCursor: ' FROM tableA;',
+        containsKeywords: ['AS', '='],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'tableA' }, { name: 'a' }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT a |, FROM tableA;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a ',
+        afterCursor: ', FROM tableA;',
+        containsKeywords: ['AS', '='],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'tableA' }, { name: 'a' }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT a, b | FROM tableA;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b ',
+        afterCursor: ' FROM tableA;',
+        containsKeywords: ['AS', '='],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'tableA' }, { name: 'b' }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT a |, b, c AS foo, d FROM tableA;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a ',
+        afterCursor: ', b, c AS foo, d FROM tableA;',
+        containsKeywords: ['AS', '='],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'tableA' }, { name: 'a' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT | a, cast(b as int), c, d FROM testTable WHERE a = \'US\' AND b >= 998 ORDER BY c DESC LIMIT 15"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor:
+          " a, cast(b as int), c, d FROM testTable WHERE a = 'US' AND b >= 998 ORDER BY c DESC LIMIT 15",
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, |,c, d FROM testTable WHERE a = \'US\' AND b >= 998 ORDER BY c DESC LIMIT 15"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, ',
+        afterCursor: ",c, d FROM testTable WHERE a = 'US' AND b >= 998 ORDER BY c DESC LIMIT 15",
+        containsKeywords: ['*', 'CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 87 }
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 22 }
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 },
+              identifierChain: [{ name: 'a' }],
+              tables: [{ identifierChain: [{ name: 'testTable' }] }],
+              qualified: false
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 11, last_column: 12 },
+              identifierChain: [{ name: 'b' }],
+              tables: [{ identifierChain: [{ name: 'testTable' }] }],
+              qualified: false
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 15, last_column: 16 },
+              identifierChain: [{ name: 'c' }],
+              tables: [{ identifierChain: [{ name: 'testTable' }] }],
+              qualified: false
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 18, last_column: 19 },
+              identifierChain: [{ name: 'd' }],
+              tables: [{ identifierChain: [{ name: 'testTable' }] }],
+              qualified: false
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 25, last_column: 34 },
+              identifierChain: [{ name: 'testTable' }]
+            },
+            {
+              type: 'whereClause',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 35, last_column: 62 }
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 41, last_column: 42 },
+              identifierChain: [{ name: 'a' }],
+              tables: [{ identifierChain: [{ name: 'testTable' }] }],
+              qualified: false
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 54, last_column: 55 },
+              identifierChain: [{ name: 'b' }],
+              tables: [{ identifierChain: [{ name: 'testTable' }] }],
+              qualified: false
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 72, last_column: 73 },
+              identifierChain: [{ name: 'c' }],
+              tables: [{ identifierChain: [{ name: 'testTable' }] }],
+              qualified: false
+            },
+            {
+              type: 'limitClause',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 79, last_column: 87 }
+            }
+          ]
+        }
+      });
+    });
+  });
+
+  describe('Variable References', () => {
+    it('should suggest tables for "SELECT | FROM ${some_variable};"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' FROM ${some_variable};',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: {
+            tables: [{ identifierChain: [{ name: '${some_variable}' }] }]
+          },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: '${some_variable}' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable WHERE ${some_variable} |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE ${some_variable} ',
+        afterCursor: '',
+        containsKeywords: ['<', 'BETWEEN'],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          suggestGroupBys: {
+            prefix: 'GROUP BY',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestOrderBys: {
+            prefix: 'ORDER BY',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: '${some_variable}' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM testTable WHERE ${some_variable} + 1 = |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE ${some_variable} + 1 = ',
+        afterCursor: '',
+        containsKeywords: ['CASE', 'NULL'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+  });
+
+  describe('Window and analytic functions', () => {
+    it('should handle "SELECT row_number() OVER (PARTITION BY a) FROM testTable;|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (PARTITION BY a) FROM testTable;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should handle "SELECT COUNT(DISTINCT a) OVER (PARTITION by c) FROM testTable;|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT COUNT(DISTINCT a) OVER (PARTITION by c) FROM testTable;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest analytical functions for "SELECT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: '',
+        containsKeywords: ['*'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestTables: { prependQuestionMark: true, prependFrom: true },
+          suggestDatabases: { prependQuestionMark: true, prependFrom: true, appendDot: true }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() ',
+        afterCursor: '',
+        containsKeywords: ['OVER'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['OVER'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() |, b, c FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() ',
+        afterCursor: ', b, c FROM testTable',
+        containsKeywords: ['OVER'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT count(DISTINCT a) |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT count(DISTINCT a) ',
+        afterCursor: '',
+        containsKeywords: ['OVER'],
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { prependFrom: true },
+          suggestDatabases: { prependFrom: true, appendDot: true }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT count(DISTINCT a) | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT count(DISTINCT a) ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['OVER'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT count(DISTINCT a) |, b, c FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT count(DISTINCT a) ',
+        afterCursor: ', b, c FROM testTable',
+        containsKeywords: ['OVER'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (| FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER ( ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['PARTITION BY'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (PARTITION ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['BY'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a, b ORDER | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (PARTITION BY a, b ORDER ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['BY'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT row_number() OVER (ORDER BY | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (ORDER BY ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT row_number() OVER (ORDER BY |) FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (ORDER BY ',
+        afterCursor: ') FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          } // TODO: source: 'order by'
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT row_number() OVER (ORDER BY foo |) FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (ORDER BY a ',
+        afterCursor: ') FROM testTable',
+        containsKeywords: ['ASC', 'DESC'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT row_number() OVER (PARTITION BY | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (PARTITION BY ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT row_number() OVER (PARTITION BY a, | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (PARTITION BY a, ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (PARTITION BY a ORDER BY b ',
+        afterCursor: '',
+        containsKeywords: ['ASC', 'ROWS BETWEEN', 'RANGE BETWEEN'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['CURRENT ROW', 'UNBOUNDED PRECEDING']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN 1 |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN 1 ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['PRECEDING']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN UNBOUNDED |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN UNBOUNDED ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['PRECEDING']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN CURRENT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN CURRENT ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['ROW']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN 1 PRECEDING |"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN 1 PRECEDING ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['AND']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN UNBOUNDED PRECEDING |"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN UNBOUNDED PRECEDING ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['AND']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN CURRENT ROW |"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN CURRENT ROW ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['AND']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN 1 PRECEDING AND |"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN 1 PRECEDING AND ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['CURRENT ROW', 'UNBOUNDED FOLLOWING']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN UNBOUNDED PRECEDING AND |"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN UNBOUNDED PRECEDING AND ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['CURRENT ROW', 'UNBOUNDED FOLLOWING']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN CURRENT ROW AND |"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN CURRENT ROW AND ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['CURRENT ROW', 'UNBOUNDED FOLLOWING']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN 1 PRECEDING AND CURRENT |"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN 1 PRECEDING AND CURRENT ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['ROW']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED |"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['FOLLOWING']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN CURRENT ROW AND 1 |"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN CURRENT ROW AND 1 ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['FOLLOWING']
+        }
+      });
+    });
+  });
+
+  describe('Functions', () => {
+    it('should suggest tables for "SELECT COUNT(*) |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT COUNT(*) ',
+        afterCursor: '',
+        containsKeywords: ['AS', '+'],
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: {
+            prependFrom: true
+          },
+          suggestDatabases: {
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT COUNT(foo |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT COUNT(foo ',
+        afterCursor: '',
+        containsKeywords: ['AND', '='],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT COUNT(foo, |) FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT COUNT(foo, ',
+        afterCursor: ') FROM bar;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'bar' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT COUNT(foo, bl|, bla) FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT COUNT(foo, bl',
+        afterCursor: ',bla) FROM bar;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'bar' }] }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT COUNT(foo, bla |, bar)"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT COUNT(foo ',
+        afterCursor: ', bar)',
+        containsKeywords: ['AND', '='],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest columns and values for "SELECT COUNT(foo, bl = |,bla) FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT COUNT(foo, bl = ',
+        afterCursor: ',bla) FROM bar;',
+        containsKeywords: ['CASE'],
+        hasErrors: false,
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'bar' }] }]
+          },
+          suggestValues: {},
+          colRef: { identifierChain: [{ name: 'bar' }, { name: 'bl' }] }
+        }
+      });
+    });
+
+    it('should suggest columns and values for "SELECT bl = \'| FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: "SELECT bl = '",
+        afterCursor: ' FROM bar;',
+        hasErrors: false,
+        expectedResult: {
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 23 }
+            },
+            {
+              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: 10 },
+              identifierChain: [{ name: 'bl' }],
+              tables: [{ identifierChain: [{ name: 'bar' }] }],
+              qualified: false
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 20, last_column: 23 },
+              identifierChain: [{ name: 'bar' }]
+            },
+            {
+              type: 'whereClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 23, last_column: 23 }
+            },
+            {
+              type: 'limitClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 23, last_column: 23 }
+            }
+          ],
+          lowerCase: false,
+          suggestValues: { partialQuote: "'", missingEndQuote: true },
+          colRef: { identifierChain: [{ name: 'bar' }, { name: 'bl' }] }
+        }
+      });
+    });
+
+    it('should suggest columns and values for "SELECT bl = \'|\' FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: "SELECT bl = '",
+        afterCursor: "' FROM bar;",
+        hasErrors: false,
+        expectedResult: {
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 24 }
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 16 }
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 10 },
+              identifierChain: [{ name: 'bl' }],
+              tables: [{ identifierChain: [{ name: 'bar' }] }],
+              qualified: false
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 21, last_column: 24 },
+              identifierChain: [{ name: 'bar' }]
+            },
+            {
+              type: 'whereClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 24, last_column: 24 }
+            },
+            {
+              type: 'limitClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 24, last_column: 24 }
+            }
+          ],
+          lowerCase: false,
+          suggestValues: { partialQuote: "'", missingEndQuote: false },
+          colRef: { identifierChain: [{ name: 'bar' }, { name: 'bl' }] }
+        }
+      });
+    });
+
+    it('should suggest columns and values for "SELECT bl = \'bl| bl\' FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: "SELECT bl = 'bl",
+        afterCursor: " bl' FROM bar;",
+        hasErrors: false,
+        expectedResult: {
+          locations: [
+            {
+              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: 19 }
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 10 },
+              identifierChain: [{ name: 'bl' }],
+              tables: [{ identifierChain: [{ name: 'bar' }] }],
+              qualified: false
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 26, last_column: 29 },
+              identifierChain: [{ name: 'bar' }]
+            },
+            {
+              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 }
+            }
+          ],
+          lowerCase: false,
+          suggestValues: { partialQuote: "'", missingEndQuote: false },
+          colRef: { identifierChain: [{ name: 'bar' }, { name: 'bl' }] }
+        }
+      });
+    });
+
+    it('should suggest columns and values for "SELECT bl = "| FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT bl = "',
+        afterCursor: ' FROM bar;',
+        hasErrors: false,
+        expectedResult: {
+          lowerCase: false,
+          suggestValues: { partialQuote: '"', missingEndQuote: true },
+          colRef: { identifierChain: [{ name: 'bar' }, { name: 'bl' }] }
+        }
+      });
+    });
+
+    it('should suggest columns and values for "SELECT bl = "|" FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT bl = "',
+        afterCursor: '" FROM bar;',
+        hasErrors: false,
+        expectedResult: {
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 24 }
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 16 }
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 10 },
+              identifierChain: [{ name: 'bl' }],
+              tables: [{ identifierChain: [{ name: 'bar' }] }],
+              qualified: false
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 21, last_column: 24 },
+              identifierChain: [{ name: 'bar' }]
+            },
+            {
+              type: 'whereClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 24, last_column: 24 }
+            },
+            {
+              type: 'limitClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 24, last_column: 24 }
+            }
+          ],
+          lowerCase: false,
+          suggestValues: { partialQuote: '"', missingEndQuote: false },
+          colRef: { identifierChain: [{ name: 'bar' }, { name: 'bl' }] }
+        }
+      });
+    });
+
+    it('should suggest columns and values for "SELECT bl = "bl| bl" FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT bl = "bl',
+        afterCursor: ' bl" FROM bar;',
+        hasErrors: false,
+        expectedResult: {
+          locations: [
+            {
+              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: 19 }
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 10 },
+              identifierChain: [{ name: 'bl' }],
+              tables: [{ identifierChain: [{ name: 'bar' }] }],
+              qualified: false
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 26, last_column: 29 },
+              identifierChain: [{ name: 'bar' }]
+            },
+            {
+              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 }
+            }
+          ],
+          lowerCase: false,
+          suggestValues: { partialQuote: '"', missingEndQuote: false },
+          colRef: { identifierChain: [{ name: 'bar' }, { name: 'bl' }] }
+        }
+      });
+    });
+
+    it('should suggest functions for "SELECT CAST(|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {}
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CAST(| FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(',
+        afterCursor: ' FROM bar;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'bar' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CAST(bla| FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(bla',
+        afterCursor: ' FROM bar;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'bar' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CAST(| AS FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(',
+        afterCursor: ' AS FROM bar;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'bar' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CAST(| AS INT FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(',
+        afterCursor: ' AS INT FROM bar;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'bar' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CAST(| AS STRING) FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(',
+        afterCursor: ' AS STRING) FROM bar;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'bar' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CAST(bla| AS STRING) FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(bla',
+        afterCursor: ' AS STRING) FROM bar;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'bar' }] }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CAST(bla |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(bla ',
+        afterCursor: '',
+        containsKeywords: ['AS', 'AND'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CAST(bla | FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(bla ',
+        afterCursor: ' FROM bar;',
+        containsKeywords: ['AS', '='],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'bar' }, { name: 'bla' }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "select cast(bla as |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select cast(bla as ',
+        afterCursor: '',
+        containsKeywords: ['INT', 'STRING'],
+        expectedResult: {
+          lowerCase: true
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CAST(bla AS | FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(bla AS ',
+        afterCursor: ' FROM bar;',
+        containsKeywords: ['INT', 'STRING'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CAST(bla AS ST|) FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(bla AS ST',
+        afterCursor: ') FROM bar;',
+        containsKeywords: ['INT', 'STRING'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CAST(AS |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(AS ',
+        afterCursor: '',
+        containsKeywords: ['INT', 'STRING'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest handle "SELECT db.customUdf(col) FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT db.customUdf(col) FROM bar;',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false,
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 34 }
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 25 }
+            },
+            {
+              type: 'database',
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 10 },
+              identifierChain: [{ name: 'db' }]
+            },
+            {
+              type: 'function',
+              location: { first_line: 1, last_line: 1, first_column: 11, last_column: 19 },
+              identifierChain: [{ name: 'db' }, { name: 'customUdf' }],
+              function: 'customudf'
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 21, last_column: 24 },
+              identifierChain: [{ name: 'col' }],
+              tables: [{ identifierChain: [{ name: 'bar' }] }],
+              qualified: false
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 31, last_column: 34 },
+              identifierChain: [{ name: 'bar' }]
+            },
+            {
+              type: 'whereClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 34, last_column: 34 }
+            },
+            {
+              type: 'limitClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 34, last_column: 34 }
+            }
+          ]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT db.customUdf(| FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT db.customUdf(',
+        afterCursor: ' FROM bar;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['T'] },
+          suggestColumns: {
+            types: ['T'],
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'bar' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT db.customUdf(1, | FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT db.customUdf(1, ',
+        afterCursor: ' FROM bar;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['T'] },
+          suggestColumns: {
+            types: ['T'],
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'bar' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT <GeneralSetFunction>(|) FROM testTable"', () => {
+      const aggregateFunctions = [
+        { name: 'AVG', containsKeywords: ['DISTINCT'] },
+        { name: 'COUNT', containsKeywords: ['*', 'DISTINCT'] },
+        { name: 'STDDEV_POP', containsKeywords: ['DISTINCT'] },
+        { name: 'STDDEV_SAMP', containsKeywords: ['DISTINCT'] },
+        { name: 'SUM', containsKeywords: ['DISTINCT'] },
+        { name: 'MAX', containsKeywords: ['DISTINCT'] },
+        { name: 'MIN', containsKeywords: ['DISTINCT'] },
+        { name: 'VAR_POP', containsKeywords: ['DISTINCT'] },
+        { name: 'var_samp', containsKeywords: ['DISTINCT'] }
+      ];
+      aggregateFunctions.forEach(aggregateFunction => {
+        if (aggregateFunction.name === 'COUNT') {
+          assertAutoComplete({
+            beforeCursor: 'SELECT ' + aggregateFunction.name + '(',
+            afterCursor: ') FROM testTable',
+            containsKeywords: aggregateFunction.containsKeywords.concat(['*', 'CASE']),
+            expectedResult: {
+              lowerCase: false,
+              suggestFunctions: {},
+              suggestColumns: {
+                source: 'select',
+                tables: [{ identifierChain: [{ name: 'testTable' }] }]
+              }
+            }
+          });
+        } else {
+          const expectedResult = {
+            lowerCase: false,
+            suggestFunctions: { types: aggregateFunction.types || ['T'] },
+            suggestColumns: {
+              source: 'select',
+              types: aggregateFunction.types || ['T'],
+              tables: [{ identifierChain: [{ name: 'testTable' }] }]
+            }
+          };
+          assertAutoComplete({
+            beforeCursor: 'SELECT ' + aggregateFunction.name + '(',
+            afterCursor: ') FROM testTable',
+            containsKeywords: aggregateFunction.containsKeywords.concat(['CASE']),
+            expectedResult: expectedResult
+          });
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT id, SUM(a * | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT id, SUM(a * ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE ',
+        afterCursor: ' FROM testTable',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestKeywords: ['WHEN']
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN a = b AND | THEN FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN a = b AND ',
+        afterCursor: ' THEN FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a = b AND | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = b AND ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CASE a = b | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = b ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['WHEN', 'AND', '<>'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN a = b OR | THEN boo FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN a = b OR ',
+        afterCursor: ' THEN boo FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN a = b OR c THEN boo OR | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN a = b OR c THEN boo OR ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a =| WHEN c THEN d END FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a =',
+        afterCursor: ' WHEN c THEN d END FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestValues: {},
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'a' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a =| WHEN c THEN d ELSE e END FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a =',
+        afterCursor: ' WHEN c THEN d ELSE e END FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestValues: {},
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'a' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a = c WHEN c THEN d | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = c WHEN c THEN d ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['END', '<>'],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'd' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a = c WHEN c THEN d=| ELSE FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = c WHEN c THEN d=',
+        afterCursor: ' ELSE FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestValues: {},
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'd' }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CASE a = c WHEN c THEN d=1 | bla=foo FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = c WHEN c THEN d=1 ',
+        afterCursor: ' bla=foo FROM testTable',
+        containsKeywords: ['AND', 'WHEN', 'ELSE', 'END', '<'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CASE a = c WHEN c THEN d=1 | bla=foo END FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = c WHEN c THEN d=1 ',
+        afterCursor: ' bla=foo FROM testTable',
+        containsKeywords: ['AND', 'WHEN', 'ELSE', '>'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a = c WHEN c THEN d ELSE | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = c WHEN c THEN d ELSE ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a = c WHEN c THEN d ELSE e AND | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = c WHEN c THEN d ELSE e AND ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE ELSE | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE ELSE ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE | ELSE a FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE ',
+        afterCursor: ' ELSE a FROM testTable',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestKeywords: ['WHEN']
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE | ELSE FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE ',
+        afterCursor: ' ELSE FROM testTable',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestKeywords: ['WHEN']
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a = c WHEN c THEN d ELSE e | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = c WHEN c THEN d ELSE e ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['END', '='],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'e' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN THEN boo OR | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN THEN boo OR ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CASE | a = b THEN FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE ',
+        afterCursor: ' a = b THEN FROM testTable',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['WHEN']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CASE | a = b THEN boo FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE ',
+        afterCursor: ' a = b THEN boo FROM testTable',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['WHEN']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CASE | THEN boo FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE ',
+        afterCursor: ' THEN boo FROM testTable',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['WHEN'],
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN | boo FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN ',
+        afterCursor: ' boo FROM testTable',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['THEN'],
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN bla| boo WHEN b THEN c END FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN bla',
+        afterCursor: ' boo WHEN b THEN c END FROM testTable',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['THEN'],
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a WHEN b THEN c WHEN | boo ELSE c FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a WHEN b THEN c WHEN ',
+        afterCursor: ' boo ELSE c FROM testTable',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['THEN'],
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a WHEN b THEN c WHEN | boo WHEN d THEN e END FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a WHEN b THEN c WHEN ',
+        afterCursor: ' boo WHEN d THEN e END FROM testTable',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['THEN'],
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CASE a WHEN b THEN c | WHEN d THEN e END FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a WHEN b THEN c ',
+        afterCursor: ' WHEN d THEN e END FROM testTable',
+        containsKeywords: ['WHEN', '<'],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'c' }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CASE a WHEN b THEN c | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a WHEN b THEN c ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['WHEN', '>'],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'c' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN | THEN FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN ',
+        afterCursor: ' THEN FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest values for "SELECT CASE WHEN | = a FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN ',
+        afterCursor: ' = a FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestValues: {},
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'a' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN ab| THEN bla ELSE foo FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN ab',
+        afterCursor: ' THEN bla ELSE foo FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE bla WHEN ab| THEN bla ELSE foo END FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE bla WHEN ab',
+        afterCursor: ' THEN bla ELSE foo END FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a WHEN | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a WHEN ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN a = | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN a = ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestValues: {},
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'a' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN a = b | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN a = b ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['AND', 'THEN', '<'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a = c WHEN c | d FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = c WHEN c ',
+        afterCursor: ' d FROM testTable',
+        containsKeywords: ['THEN', '>'],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'c' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a = c WHEN c THEN | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = c WHEN c THEN ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a = c WHEN c THEN | g FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = c WHEN c THEN ',
+        afterCursor: ' g FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN THEN | g FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN THEN ',
+        afterCursor: ' g FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN THEN | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN THEN ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+  });
+
+  describe('Value Expression Completion', () => {
+    it("should suggest functions for \"SELECT 'boo \\' baa' = |\"", () => {
+      assertAutoComplete({
+        beforeCursor: "SELECT 'boo \\' baa' = ",
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['STRING'] }
+        }
+      });
+    });
+
+    it('should suggest functions for "SELECT "boo \\" baa" = |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT "boo \\" baa" = ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['STRING'] }
+        }
+      });
+    });
+
+    it('should suggest identifiers for "SELECT 1 = | OR false FROM tableOne boo, tableTwo baa;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT 1 = ',
+        afterCursor: ' OR false FROM tableOne boo, tableTwo baa;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['NUMBER'],
+            tables: [
+              { identifierChain: [{ name: 'tableOne' }], alias: 'boo' },
+              { identifierChain: [{ name: 'tableTwo' }], alias: 'baa' }
+            ]
+          },
+          suggestIdentifiers: [{ name: 'boo.', type: 'alias' }, { name: 'baa.', type: 'alias' }]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM tbl1, tbl2 atbl2, tbl3 WHERE id = atbl2.|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM tbl1, tbl2 atbl2, tbl3 WHERE id = atbl2.',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestColumns: {
+            source: 'where',
+            types: ['T'],
+            tables: [{ identifierChain: [{ name: 'tbl2' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM tbl1, tbl2 atbl2, tbl3 WHERE id = atbl2.|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM tbl1, tbl2 atbl2, tbl3 WHERE id = atbl2.',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestColumns: {
+            source: 'where',
+            types: ['T'],
+            tables: [{ identifierChain: [{ name: 'tbl2' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest values for "SELECT * FROM testTable WHERE id = |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE id =',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'id' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM testTable WHERE -|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE -',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT -| FROM testTable WHERE id = 1;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT -',
+        afterCursor: ' FROM testTable WHERE id = 1;',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT 1 < | FROM testTable WHERE id = 1;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT 1 < ',
+        afterCursor: ' FROM testTable WHERE id = 1;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "select foo from tbl where | % 2 = 0"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select foo from tbl where ',
+        afterCursor: ' % 2 = 0',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: true,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'tbl' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest values for "SELECT * FROM testTable WHERE -id = |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE -id = ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'id' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT greatest(1, 2, a, 4, | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT greatest(1, 2, a, 4, ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['T'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['T'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT greatest(1, |, a, 4) FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT greatest(1, ',
+        afterCursor: ', a, 4) FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['T'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['T'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT | > id FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' > id FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestValues: {},
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'id' }] }
+        }
+      });
+    });
+
+    it('should suggest values and columns for "SELECT * FROM testTable WHERE | = id"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE ',
+        afterCursor: ' = id',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'id' }] }
+        }
+      });
+    });
+
+    it('should suggest values and columns for "SELECT a, b, c FROM testTable WHERE d = |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d >= ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'd' }] }
+        }
+      });
+    });
+
+    it('should suggest values and columns for "SELECT a, b, c FROM testTable WHERE d < |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d < ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'd' }] }
+        }
+      });
+    });
+
+    it('should suggest values and columns for "SELECT a, b, c FROM testTable WHERE d <= |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d <= ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'd' }] }
+        }
+      });
+    });
+
+    it('should suggest values and columns for "SELECT a, b, c FROM testTable WHERE d <=> |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d <=> ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'd' }] }
+        }
+      });
+    });
+
+    it('should suggest values and columns for "SELECT a, b, c FROM testTable WHERE d <> |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d <> ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'd' }] }
+        }
+      });
+    });
+
+    it('should suggest values and columns for "SELECT a, b, c FROM testTable WHERE d >= |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d >= ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'd' }] }
+        }
+      });
+    });
+
+    it('should suggest values and columns for "SELECT a, b, c FROM testTable WHERE d > |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d > ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'd' }] }
+        }
+      });
+    });
+
+    it('should suggest values and columns for "SELECT a, b, c FROM testTable WHERE d != |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d != ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'd' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE d + 1 != |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d + 1 != ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE bla| + 1 != 3"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE bla',
+        afterCursor: ' + 1 != 3',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE d + |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d + ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE d - |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d - ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE d * |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d * ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE d / |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d / ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE d % |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d % ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE d | |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d | ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE d & |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d & ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE d ^ |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d ^ ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE ~|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE ~',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE -|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE -',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    // TODO: This one causes an unrecoverable error after the cursor, we should suggest group by etc.
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE d | RLIKE \'bla bla\'"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d ',
+        afterCursor: " RLIKE 'bla bla'",
+        containsKeywords: ['<', 'IN'],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'd' }] },
+          suggestGroupBys: {
+            prefix: 'GROUP BY',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestOrderBys: {
+            prefix: 'ORDER BY',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT bar FROM foo WHERE id = 1 |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT bar FROM foo WHERE id = 1 ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestGroupBys: { prefix: 'GROUP BY', tables: [{ identifierChain: [{ name: 'foo' }] }] },
+          suggestOrderBys: { prefix: 'ORDER BY', tables: [{ identifierChain: [{ name: 'foo' }] }] },
+          suggestKeywords: [
+            'GROUP BY',
+            'HAVING',
+            'ORDER BY',
+            'LIMIT',
+            'UNION',
+            '<',
+            '<=',
+            '<=>',
+            '<>',
+            '=',
+            '>',
+            '>=',
+            'AND',
+            'BETWEEN',
+            'IN',
+            'IS FALSE',
+            'IS NOT FALSE',
+            'IS NOT NULL',
+            'IS NOT TRUE',
+            'IS NULL',
+            'IS TRUE',
+            'NOT BETWEEN',
+            'NOT IN',
+            'OR'
+          ]
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM foo WHERE id <=> 1 |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE id <=> 1 ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestGroupBys: { prefix: 'GROUP BY', tables: [{ identifierChain: [{ name: 'foo' }] }] },
+          suggestOrderBys: { prefix: 'ORDER BY', tables: [{ identifierChain: [{ name: 'foo' }] }] },
+          suggestKeywords: [
+            'GROUP BY',
+            'HAVING',
+            'ORDER BY',
+            'LIMIT',
+            'UNION',
+            '<',
+            '<=',
+            '<=>',
+            '<>',
+            '=',
+            '>',
+            '>=',
+            'AND',
+            'BETWEEN',
+            'IN',
+            'IS FALSE',
+            'IS NOT FALSE',
+            'IS NOT NULL',
+            'IS NOT TRUE',
+            'IS NULL',
+            'IS TRUE',
+            'NOT BETWEEN',
+            'NOT IN',
+            'OR'
+          ]
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM foo WHERE id IS |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE id IS ',
+        afterCursor: '',
+        containsKeywords: ['NOT NULL', 'NULL', 'NOT TRUE', 'TRUE', 'NOT FALSE', 'FALSE'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM foo WHERE id IS NOT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE id IS NOT ',
+        afterCursor: '',
+        containsKeywords: ['NULL', 'FALSE', 'TRUE'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM foo WHERE id IS | NULL"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE id IS ',
+        afterCursor: ' NULL',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['NOT']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM foo WHERE id IS | FALSE"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE id IS ',
+        afterCursor: ' FALSE',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['NOT']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM foo WHERE id IS | TRUE"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE id IS ',
+        afterCursor: ' TRUE',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['NOT']
+        }
+      });
+    });
+
+    // Fails because "NOT" is missing
+    xit('should suggest keywords for "SELECT * FROM foo WHERE id | LIKE \'bla bla\'"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE id ',
+        afterCursor: " LIKE 'bla bla'",
+        containsKeywords: ['<', 'IN', 'NOT'],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'id' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM foo WHERE id LIKE |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE id LIKE ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['STRING'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['STRING'],
+            tables: [{ identifierChain: [{ name: 'foo' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM foo WHERE id LIKE \'\' GROUP |"', () => {
+      assertAutoComplete({
+        beforeCursor: "SELECT * FROM foo WHERE id LIKE '' GROUP ",
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestGroupBys: { prefix: 'BY', tables: [{ identifierChain: [{ name: 'foo' }] }] },
+          suggestKeywords: ['BY']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM foo WHERE id LIKE (\'bla bla\') |"', () => {
+      assertAutoComplete({
+        beforeCursor: "SELECT * FROM foo WHERE id LIKE ('bla bla') ",
+        afterCursor: '',
+        containsKeywords: ['AND'],
+        expectedResult: {
+          lowerCase: false,
+          suggestGroupBys: { prefix: 'GROUP BY', tables: [{ identifierChain: [{ name: 'foo' }] }] },
+          suggestOrderBys: { prefix: 'ORDER BY', tables: [{ identifierChain: [{ name: 'foo' }] }] }
+        }
+      });
+    });
+
+    it('should suggest identifiers for "SELECT * FROM foo bla, bar WHERE id IS NULL AND |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo bla, bar WHERE id IS NULL AND ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestFilters: {
+            tables: [
+              { identifierChain: [{ name: 'foo' }], alias: 'bla' },
+              { identifierChain: [{ name: 'bar' }] }
+            ]
+          },
+          suggestColumns: {
+            source: 'where',
+            tables: [
+              { identifierChain: [{ name: 'foo' }], alias: 'bla' },
+              { identifierChain: [{ name: 'bar' }] }
+            ]
+          },
+          suggestIdentifiers: [{ name: 'bla.', type: 'alias' }, { name: 'bar.', type: 'table' }]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM foo AS bla WHERE id IS NULL && |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo AS bla WHERE id IS NULL && ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestFilters: { tables: [{ identifierChain: [{ name: 'foo' }], alias: 'bla' }] },
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'foo' }], alias: 'bla' }]
+          },
+          suggestIdentifiers: [{ name: 'bla.', type: 'alias' }]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM foo AS bla WHERE id IS NULL OR | AND 1 + 1 > 1"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo AS bla WHERE id IS NULL OR ',
+        afterCursor: ' AND 1 + 1 > 1',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestFilters: { tables: [{ identifierChain: [{ name: 'foo' }], alias: 'bla' }] },
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'foo' }], alias: 'bla' }]
+          },
+          suggestIdentifiers: [{ name: 'bla.', type: 'alias' }]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM foo AS bla WHERE id IS NULL || |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo AS bla WHERE id IS NULL || ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestFilters: { tables: [{ identifierChain: [{ name: 'foo' }], alias: 'bla' }] },
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'foo' }], alias: 'bla' }]
+          },
+          suggestIdentifiers: [{ name: 'bla.', type: 'alias' }]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM foo bar WHERE NOT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo bar WHERE NOT ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'foo' }], alias: 'bar' }]
+          },
+          suggestKeywords: ['EXISTS'],
+          suggestIdentifiers: [{ name: 'bar.', type: 'alias' }]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM foo bar WHERE ! |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo bar WHERE ! ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['BOOLEAN'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['BOOLEAN'],
+            tables: [{ identifierChain: [{ name: 'foo' }], alias: 'bar' }]
+          },
+          suggestIdentifiers: [{ name: 'bar.', type: 'alias' }]
+        }
+      });
+    });
+  });
+
+  describe('Field Completion', () => {
+    it('should suggest columns for "SELECT * FROM testTable WHERE |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestKeywords: ['EXISTS', 'NOT EXISTS'],
+          suggestFilters: { tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM testTable WHERE a|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE a',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestKeywords: ['EXISTS', 'NOT EXISTS'],
+          suggestFilters: { tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM testTable WHERE baa = 1 AND |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE baa = 1 AND ',
+        afterCursor: '',
+        containsKeywords: ['CASE', 'EXISTS', 'NOT', 'NULL'],
+        expectedResult: {
+          lowerCase: false,
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestFunctions: {},
+          suggestFilters: { tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM testTable WHERE | AND baa = 1"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE ',
+        afterCursor: ' AND baa = 1',
+        containsKeywords: ['CASE', 'EXISTS', 'NOT', 'NULL'],
+        expectedResult: {
+          lowerCase: false,
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestFunctions: {},
+          suggestFilters: { tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM testTable WHERE baa = 1 OR |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE baa = 1 OR ',
+        afterCursor: '',
+        containsKeywords: ['CASE', 'EXISTS', 'NOT', 'NULL'],
+        expectedResult: {
+          lowerCase: false,
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestFunctions: {},
+          suggestFilters: { tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM testTable WHERE | OR baa = 1"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE ',
+        afterCursor: ' OR baa = 1',
+        containsKeywords: ['CASE', 'EXISTS', 'NOT', 'NULL'],
+        expectedResult: {
+          lowerCase: false,
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestFunctions: {},
+          suggestFilters: { tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable WHERE NOT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE NOT ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestKeywords: ['EXISTS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable WHERE foo = \'bar\' |"', () => {
+      assertAutoComplete({
+        beforeCursor: "SELECT * FROM testTable WHERE foo = 'bar' ",
+        afterCursor: '',
+        containsKeywords: ['AND', '<'],
+        expectedResult: {
+          lowerCase: false,
+          suggestGroupBys: {
+            prefix: 'GROUP BY',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestOrderBys: {
+            prefix: 'ORDER BY',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT a, b, c, d, e FROM tableOne WHERE c >= 9998 an|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c, d, e FROM tableOne WHERE c >= 9998 an',
+        afterCursor: '',
+        containsKeywords: ['AND', '='],
+        expectedResult: {
+          lowerCase: false,
+          suggestGroupBys: {
+            prefix: 'GROUP BY',
+            tables: [{ identifierChain: [{ name: 'tableOne' }] }]
+          },
+          suggestOrderBys: {
+            prefix: 'ORDER BY',
+            tables: [{ identifierChain: [{ name: 'tableOne' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM testTable WHERE foo = \'bar\' AND |"', () => {
+      assertAutoComplete({
+        beforeCursor: "SELECT * FROM testTable WHERE foo = 'bar' AND ",
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestFilters: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, \\nc,\\nd, |\\ng,\\nf\\nFROM testTable WHERE a > 1 AND b = \'b\' ORDER BY c;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, \nc,\nd, ',
+        afterCursor: "\ng,\nf\nFROM testTable WHERE a > 1 AND b = 'b' ORDER BY c;",
+        containsKeywords: ['*', 'CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a,b, | c FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a,b, ',
+        afterCursor: ' c FROM testTable',
+        containsKeywords: ['*'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT | a, b, c FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' a, b, c FROM testTable',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT a |, b, c FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a ',
+        afterCursor: ', b, c FROM testTable',
+        containsKeywords: ['AS', '>'],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'a' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM testTable WHERE | = \'bar\' AND "', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE ',
+        afterCursor: " = 'bar' AND ",
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['STRING'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['STRING'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable WHERE a |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE a ',
+        afterCursor: '',
+        containsKeywords: ['BETWEEN', 'NOT BETWEEN'],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'a' }] },
+          suggestGroupBys: {
+            prefix: 'GROUP BY',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestOrderBys: {
+            prefix: 'ORDER BY',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable WHERE a NOT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE a NOT ',
+        afterCursor: '',
+        containsKeywords: ['BETWEEN', 'EXISTS', 'IN', 'LIKE'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest values for "SELECT * FROM testTable WHERE a BETWEEN |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE a BETWEEN ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'a' }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable WHERE a OR NOT EXISTS (|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE a OR NOT EXISTS (',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['SELECT']
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT t1.testTableColumn1, t2.testTableColumn3 FROM testTable1 t1 JOIN testTable2 t2 ON t1.|"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT t1.testTableColumn1, t2.testTableColumn3 FROM testTable1 t1 JOIN testTable2 t2 ON t1.',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestColumns: { tables: [{ identifierChain: [{ name: 'testTable1' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT t1.testTableColumn1, t2.testTableColumn3 FROM database_two.testTable1 t1 JOIN testTable2 t2 ON t1.|"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT t1.testTableColumn1, t2.testTableColumn3 FROM database_two.testTable1 t1 JOIN testTable2 t2 ON t1.',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestColumns: {
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable1' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT testTable.| FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT testTable.',
+        afterCursor: ' FROM testTable',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['*'],
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns "SELECT tt.| FROM testTable tt"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT tt.',
+        afterCursor: ' FROM testTable tt',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['*'],
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT tt.| FROM database_two.testTable tt"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT tt.',
+        afterCursor: ' FROM database_two.testTable tt',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['*'],
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT tta.| FROM testTableA tta, testTableB ttb"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT tta.',
+        afterCursor: ' FROM testTableA tta, testTableB ttb',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['*'],
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTableA' }] }]
+          }
+        }
+      });
+      assertAutoComplete({
+        beforeCursor: 'SELECT ttb.',
+        afterCursor: ' FROM testTableA tta, testTableB ttb',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['*'],
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTableB' }] }]
+          }
+        }
+      });
+    });
+  });
+
+  describe('ORDER BY Clause', () => {
+    it('should suggest keywords for "SELECT * FROM testTable GROUP BY a | LIMIT 10"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable GROUP BY a ',
+        afterCursor: ' LIMIT 10',
+        doesNotContainKeywords: ['LIMIT'],
+        containsKeywords: ['ORDER BY'],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          suggestOrderBys: {
+            prefix: 'ORDER BY',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'a' }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable ORDER |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable ORDER ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['BY'],
+          suggestOrderBys: { prefix: 'BY', tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM testTable ORDER BY |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable ORDER BY ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestAnalyticFunctions: true,
+          suggestColumns: {
+            source: 'order by',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestOrderBys: { tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM database_two.testTable ORDER BY |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM database_two.testTable ORDER BY ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          suggestColumns: {
+            source: 'order by',
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable' }] }]
+          },
+          suggestFunctions: {},
+          suggestAnalyticFunctions: true,
+          suggestOrderBys: {
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable' }] }]
+          },
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM database_two.testTable ORDER BY foo |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM database_two.testTable ORDER BY foo ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['ASC', 'DESC', 'LIMIT', 'UNION']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM database_two.testTable ORDER BY foo + |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM database_two.testTable ORDER BY foo + ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'order by',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM database_two.testTable ORDER BY foo, |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM database_two.testTable ORDER BY foo, ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestAnalyticFunctions: true,
+          suggestColumns: {
+            source: 'order by',
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM database_two.testTable ORDER BY foo + baa ASC, |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM database_two.testTable ORDER BY foo + baa ASC, ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestAnalyticFunctions: true,
+          suggestColumns: {
+            source: 'order by',
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM database_two.testTable ORDER BY foo ASC, |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM database_two.testTable ORDER BY foo ASC, ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestAnalyticFunctions: true,
+          suggestColumns: {
+            source: 'order by',
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM database_two.testTable ORDER BY foo DESC, bar |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM database_two.testTable ORDER BY foo DESC, bar ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['ASC', 'DESC', 'LIMIT', 'UNION']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM database_two.testTable ORDER BY foo DESC, bar |, bla"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM database_two.testTable ORDER BY foo DESC, bar ',
+        afterCursor: ', bla',
+        containsKeywords: ['ASC', 'DESC'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM database_two.testTable ORDER BY foo LIMIT 10 |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM database_two.testTable ORDER BY foo LIMIT 10 ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['UNION']
+        }
+      });
+    });
+  });
+
+  describe('GROUP BY Clause', () => {
+    it('should suggest keywords for "SELECT * FROM testTable GROUP |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable GROUP ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['BY'],
+          suggestGroupBys: { prefix: 'BY', tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+        }
+      });
+    });
+
+    it('should suggest identifiers for "SELECT * FROM testTableA tta, testTableB GROUP BY |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTableA tta, testTableB GROUP BY ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'group by',
+            tables: [
+              { identifierChain: [{ name: 'testTableA' }], alias: 'tta' },
+              { identifierChain: [{ name: 'testTableB' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'tta.', type: 'alias' },
+            { name: 'testTableB.', type: 'table' }
+          ],
+          suggestGroupBys: {
+            tables: [
+              { identifierChain: [{ name: 'testTableA' }], alias: 'tta' },
+              { identifierChain: [{ name: 'testTableB' }] }
+            ]
+          }
+        }
+      });
+    });
+
+    it('should suggest identifier for "SELECT * FROM testTableA tta, testTableB GROUP BY bla, |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTableA tta, testTableB GROUP BY bla, ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'group by',
+            tables: [
+              { identifierChain: [{ name: 'testTableA' }], alias: 'tta' },
+              { identifierChain: [{ name: 'testTableB' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'tta.', type: 'alias' },
+            { name: 'testTableB.', type: 'table' }
+          ]
+        }
+      });
+    });
+
+    it('should suggest identifier for "SELECT * FROM testTableA tta, testTableB GROUP BY bla+|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTableA tta, testTableB GROUP BY bla+',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'group by',
+            types: ['NUMBER'],
+            tables: [
+              { identifierChain: [{ name: 'testTableA' }], alias: 'tta' },
+              { identifierChain: [{ name: 'testTableB' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'tta.', type: 'alias' },
+            { name: 'testTableB.', type: 'table' }
+          ]
+        }
+      });
+    });
+
+    it('should suggest identifier for "SELECT * FROM testTableA tta, testTableB GROUP BY bla+foo, |, foo"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTableA tta, testTableB GROUP BY bla, ',
+        afterCursor: ', foo',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'group by',
+            tables: [
+              { identifierChain: [{ name: 'testTableA' }], alias: 'tta' },
+              { identifierChain: [{ name: 'testTableB' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'tta.', type: 'alias' },
+            { name: 'testTableB.', type: 'table' }
+          ]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM testTable GROUP BY |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable GROUP BY ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'group by',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestGroupBys: { tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM database_two.testTable GROUP BY |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM database_two.testTable GROUP BY ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'group by',
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable' }] }]
+          },
+          suggestGroupBys: {
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+  });
+
+  describe('LIMIT clause', () => {
+    it('should not suggest anything for "SELECT COUNT(*) AS boo FROM testTable GROUP BY baa LIMIT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT COUNT(*) AS boo FROM testTable GROUP BY baa LIMIT ',
+        afterCursor: '',
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+  });
+
+  describe('UNION clause', () => {
+    // TODO: Fix locations
+    xit('should handle "SELECT * FROM (SELECT x FROM few_ints UNION ALL SELECT x FROM few_ints) AS t1 ORDER BY x;|', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT * FROM (SELECT x FROM few_ints UNION ALL SELECT x FROM few_ints) AS t1 ORDER BY x;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should handle "SELECT key FROM (SELECT key FROM src ORDER BY key LIMIT 10)subq1 UNION SELECT key FROM (SELECT key FROM src1 ORDER BY key LIMIT 10)subq2;|', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT key FROM (SELECT key FROM src ORDER BY key LIMIT 10)subq1 UNION SELECT key FROM (SELECT key FROM src1 ORDER BY key LIMIT 10)subq2;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should handle "SELECT * FROM t1 UNION DISTINCT SELECT * FROM t2;|', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM t1 UNION DISTINCT SELECT * FROM t2;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should handle "SELECT * FROM t1 UNION SELECT * FROM t2;|', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM t1 UNION SELECT * FROM t2;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM t1 UNION |', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM t1 UNION ',
+        afterCursor: '',
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['ALL', 'DISTINCT', 'SELECT']
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT * FROM t1 UNION ALL SELECT |', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM t1 UNION ALL SELECT ',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+  });
+
+  describe('WITH clause', () => {
+    it('should handle "WITH q1 AS ( SELECT key FROM src WHERE something) SELECT * FROM q1;|', () => {
+      assertAutoComplete({
+        beforeCursor: 'WITH q1 AS ( SELECT key FROM src WHERE something) SELECT * FROM q1;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should handle "WITH q1 AS (SELECT * FROM src WHERE something), q2 AS (SELECT * FROM src s2 WHERE something) SELECT * FROM q1 UNION ALL SELECT * FROM q2;|', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'WITH q1 AS (SELECT * FROM src WHERE something), q2 AS (SELECT * FROM src s2 WHERE something) SELECT * FROM q1 UNION ALL SELECT * FROM q2;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should handle "WITH t1 AS (SELECT 1) (WITH t2 AS (SELECT 2) SELECT * FROM t2) UNION ALL SELECT * FROM t1;|', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'WITH t1 AS (SELECT 1) (WITH t2 AS (SELECT 2) SELECT * FROM t2) UNION ALL SELECT * FROM t1;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "WITH t1 |', () => {
+      assertAutoComplete({
+        beforeCursor: 'WITH t1 ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['AS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "WITH t1 AS (|', () => {
+      assertAutoComplete({
+        beforeCursor: 'WITH t1 AS (',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['SELECT']
+        }
+      });
+    });
+
+    it('should suggest keywords for "WITH t1 AS (SELECT * FROM boo) |', () => {
+      assertAutoComplete({
+        beforeCursor: 'WITH t1 AS (SELECT * FROM boo) ',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest identifiers for "WITH t1 AS (SELECT * FROM FOO) SELECT |', () => {
+      assertAutoComplete({
+        beforeCursor: 'WITH t1 AS (SELECT * FROM FOO) SELECT ',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestTables: { prependQuestionMark: true, prependFrom: true },
+          suggestDatabases: { prependQuestionMark: true, prependFrom: true, appendDot: true },
+          suggestCommonTableExpressions: [
+            { name: 't1', prependFrom: true, prependQuestionMark: true }
+          ],
+          commonTableExpressions: [
+            { alias: 't1', columns: [{ tables: [{ identifierChain: [{ name: 'FOO' }] }] }] }
+          ]
+        }
+      });
+    });
+
+    it('should suggest identifiers for "WITH t1 AS (SELECT * FROM FOO), t2 AS (SELECT |', () => {
+      assertAutoComplete({
+        beforeCursor: 'WITH t1 AS (SELECT * FROM FOO), t2 AS (SELECT ',
+        afterCursor: '',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestTables: { prependQuestionMark: true, prependFrom: true },
+          suggestDatabases: { prependQuestionMark: true, prependFrom: true, appendDot: true },
+          lowerCase: false,
+          suggestCommonTableExpressions: [
+            { name: 't1', prependFrom: true, prependQuestionMark: true }
+          ],
+          commonTableExpressions: [
+            { alias: 't1', columns: [{ tables: [{ identifierChain: [{ name: 'FOO' }] }] }] }
+          ]
+        }
+      });
+    });
+
+    it('should suggest identifiers for "WITH t1 AS (SELECT * FROM FOO) SELECT * FROM |', () => {
+      assertAutoComplete({
+        beforeCursor: 'WITH t1 AS (SELECT * FROM FOO) SELECT * FROM ',
+        afterCursor: '',
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: {},
+          suggestDatabases: { appendDot: true },
+          suggestCommonTableExpressions: [{ name: 't1' }],
+          commonTableExpressions: [
+            { alias: 't1', columns: [{ tables: [{ identifierChain: [{ name: 'FOO' }] }] }] }
+          ]
+        }
+      });
+    });
+
+    it('should suggest keywords for "with s as (select * from foo join bar) select * from |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'with s as (select * from foo join bar) select * from ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: true,
+          suggestTables: {},
+          suggestDatabases: { appendDot: true },
+          commonTableExpressions: [
+            {
+              columns: [
+                {
+                  tables: [
+                    { identifierChain: [{ name: 'foo' }] },
+                    { identifierChain: [{ name: 'bar' }] }
+                  ]
+                }
+              ],
+              alias: 's'
+            }
+          ],
+          suggestCommonTableExpressions: [{ name: 's' }]
+        }
+      });
+    });
+
+    it('should suggest keywords for "with s as (select * from foo join bar) select * from |;', () => {
+      assertAutoComplete({
+        beforeCursor: 'with s as (select * from foo join bar) select * from ',
+        afterCursor: ';',
+        expectedResult: {
+          lowerCase: true,
+          suggestTables: {},
+          suggestDatabases: { appendDot: true },
+          commonTableExpressions: [
+            {
+              columns: [
+                {
+                  tables: [
+                    { identifierChain: [{ name: 'foo' }] },
+                    { identifierChain: [{ name: 'bar' }] }
+                  ]
+                }
+              ],
+              alias: 's'
+            }
+          ],
+          suggestCommonTableExpressions: [{ name: 's' }]
+        }
+      });
+    });
+  });
+
+  describe('Joins', () => {
+    it('should suggest tables for "SELECT * FROM testTable1 JOIN |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 JOIN ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestJoins: {
+            prependJoin: false,
+            joinType: 'JOIN',
+            tables: [{ identifierChain: [{ name: 'testTable1' }] }]
+          },
+          suggestTables: {},
+          suggestDatabases: { appendDot: true }
+        }
+      });
+    });
+
+    it('should suggest joins for "SELECT * FROM testTable1 JOIN testTable2 JOIN |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 JOIN testTable2 JOIN ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestJoins: {
+            prependJoin: false,
+            joinType: 'JOIN',
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'testTable2' }] }
+            ]
+          },
+          suggestTables: {},
+          suggestDatabases: { appendDot: true }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable1 INNER |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 INNER ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['JOIN']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable1 FULL |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 FULL ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['JOIN', 'OUTER JOIN']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable1 FULL OUTER |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 FULL OUTER ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['JOIN']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable1 LEFT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 LEFT ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['JOIN', 'OUTER JOIN']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable1 LEFT OUTER |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 LEFT OUTER ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['JOIN']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable1 RIGHT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 RIGHT ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['JOIN', 'OUTER JOIN']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable1 RIGHT OUTER |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 RIGHT OUTER ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['JOIN']
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT * FROM testTable1 JOIN db1.|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 JOIN db1.',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { identifierChain: [{ name: 'db1' }] }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT * FROM testTable1 JOIN db1.| JOIN foo"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 JOIN db1.',
+        afterCursor: ' JOIN foo',
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { identifierChain: [{ name: 'db1' }] }
+        }
+      });
+    });
+
+    it('should suggest join conditions for "SELECT testTable1.* FROM testTable1 JOIN testTable2 |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT testTable1.* FROM testTable1 JOIN testTable2 ',
+        afterCursor: '',
+        containsKeywords: ['ON'],
+        expectedResult: {
+          lowerCase: false,
+          suggestJoinConditions: {
+            prependOn: true,
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'testTable2' }] }
+            ]
+          },
+          suggestFilters: {
+            prefix: 'WHERE',
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'testTable2' }] }
+            ]
+          },
+          suggestGroupBys: {
+            prefix: 'GROUP BY',
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'testTable2' }] }
+            ]
+          },
+          suggestOrderBys: {
+            prefix: 'ORDER BY',
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'testTable2' }] }
+            ]
+          }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT testTable1.* FROM testTable1 JOIN testTable2 ON |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT testTable1.* FROM testTable1 JOIN testTable2 ON ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          suggestColumns: {
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'testTable2' }] }
+            ]
+          },
+          suggestFunctions: {},
+          suggestJoinConditions: {
+            prependOn: false,
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'testTable2' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'testTable1.', type: 'table' },
+            { name: 'testTable2.', type: 'table' }
+          ],
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT testTable1.* FROM testTable1 JOIN testTable2 ON (|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT testTable1.* FROM testTable1 JOIN testTable2 ON (',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'testTable2' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'testTable1.', type: 'table' },
+            { name: 'testTable2.', type: 'table' }
+          ]
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT testTable1.* FROM testTable1 JOIN testTable2 ON (testTable1.testColumn1 = testTable2.testColumn3 AND |"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT testTable1.* FROM testTable1 JOIN testTable2 ON (testTable1.testColumn1 = testTable2.testColumn3 AND ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'testTable2' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'testTable1.', type: 'table' },
+            { name: 'testTable2.', type: 'table' }
+          ]
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT testTable1.* FROM testTable1 JOIN testTable2 ON (| AND testTable1.testColumn1 = testTable2.testColumn3"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT testTable1.* FROM testTable1 JOIN testTable2 ON (',
+        afterCursor: ' AND testTable1.testColumn1 = testTable2.testColumn3',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 109 }
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 20 }
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 18 },
+              identifierChain: [{ name: 'testTable1' }]
+            },
+            {
+              type: 'asterisk',
+              location: { first_line: 1, last_line: 1, first_column: 19, last_column: 20 },
+              tables: [{ identifierChain: [{ name: 'testTable1' }] }]
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 26, last_column: 36 },
+              identifierChain: [{ name: 'testTable1' }]
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 42, last_column: 52 },
+              identifierChain: [{ name: 'testTable2' }]
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 62, last_column: 72 },
+              identifierChain: [{ name: 'testTable1' }]
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 73, last_column: 84 },
+              identifierChain: [{ name: 'testColumn1' }],
+              tables: [{ identifierChain: [{ name: 'testTable1' }] }],
+              qualified: true
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 87, last_column: 97 },
+              identifierChain: [{ name: 'testTable2' }]
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 98, last_column: 109 },
+              identifierChain: [{ name: 'testColumn3' }],
+              tables: [{ identifierChain: [{ name: 'testTable2' }] }],
+              qualified: true
+            },
+            {
+              type: 'whereClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 109, last_column: 109 }
+            },
+            {
+              type: 'limitClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 109, last_column: 109 }
+            }
+          ],
+          suggestColumns: {
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'testTable2' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'testTable1.', type: 'table' },
+            { name: 'testTable2.', type: 'table' }
+          ],
+          suggestFunctions: {},
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest identifiers for "select * from testTable1 join db.testTable2 on |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select * from testTable1 join db.testTable2 on ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: true,
+          suggestFunctions: {},
+          suggestJoinConditions: {
+            prependOn: false,
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'db' }, { name: 'testTable2' }] }
+            ]
+          },
+          suggestColumns: {
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'db' }, { name: 'testTable2' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'testTable1.', type: 'table' },
+            { name: 'testTable2.', type: 'table' }
+          ]
+        }
+      });
+    });
+
+    it('should suggest identifiers for "select * from testTable1 JOIN testTable2 on (testTable1.testColumn1 = |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select * from testTable1 JOIN testTable2 on (testTable1.testColumn1 = ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          suggestValues: {},
+          colRef: { identifierChain: [{ name: 'testTable1' }, { name: 'testColumn1' }] },
+          suggestFunctions: { types: ['COLREF'] },
+          suggestColumns: {
+            types: ['COLREF'],
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'testTable2' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'testTable1.', type: 'table' },
+            { name: 'testTable2.', type: 'table' }
+          ],
+          lowerCase: true
+        }
+      });
+    });
+
+    it('should suggest columns for "select * from testTable1 JOIN testTable2 on (testTable1.testColumn1 = testTable2.|"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'select * from testTable1 JOIN testTable2 on (testTable1.testColumn1 = testTable2.',
+        afterCursor: '',
+        ignoreErrors: true,
+        expectedResult: {
+          lowerCase: true,
+          colRef: { identifierChain: [{ name: 'testTable1' }, { name: 'testColumn1' }] },
+          suggestColumns: {
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable2' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT testTable1.* FROM testTable1 JOIN testTable2 ON (testTable1.testColumn1 = testTable2.testColumn3 AND testTable1.|"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT testTable1.* FROM testTable1 JOIN testTable2 ON (testTable1.testColumn1 = testTable2.testColumn3 AND testTable1.',
+        afterCursor: '',
+        ignoreErrors: true,
+        expectedResult: {
+          lowerCase: false,
+          suggestColumns: { tables: [{ identifierChain: [{ name: 'testTable1' }] }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT t1.* FROM table1 t1 | JOIN"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT t1.* FROM table1 t1 ',
+        afterCursor: ' JOIN',
+        expectedResult: {
+          lowerCase: false,
+          suggestJoins: {
+            prependJoin: true,
+            tables: [{ identifierChain: [{ name: 'table1' }], alias: 't1' }]
+          },
+          suggestKeywords: [
+            'FULL',
+            'FULL OUTER',
+            'INNER',
+            'LEFT',
+            'LEFT OUTER',
+            'RIGHT',
+            'RIGHT OUTER'
+          ]
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT t1.* FROM table1 t1 | JOIN table2 t2 ON t1.bla = t2.bla"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT t1.* FROM table1 t1 ',
+        afterCursor: ' JOIN table2 t2 ON t1.bla = t2.bla',
+        expectedResult: {
+          lowerCase: false,
+          suggestJoins: {
+            prependJoin: true,
+            tables: [{ identifierChain: [{ name: 'table1' }], alias: 't1' }]
+          },
+          suggestKeywords: [
+            'FULL',
+            'FULL OUTER',
+            'INNER',
+            'LEFT',
+            'LEFT OUTER',
+            'RIGHT',
+            'RIGHT OUTER'
+          ]
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT t1.* FROM table1 t1 JOIN table2 t2 | JOIN table3"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT t1.* FROM table1 t1 JOIN table2 t2 ',
+        afterCursor: ' JOIN table3',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: [
+            'ON',
+            'FULL',
+            'FULL OUTER',
+            'INNER',
+            'LEFT',
+            'LEFT OUTER',
+            'RIGHT',
+            'RIGHT OUTER'
+          ],
+          suggestJoinConditions: {
+            prependOn: true,
+            tables: [
+              { identifierChain: [{ name: 'table1' }], alias: 't1' },
+              { identifierChain: [{ name: 'table2' }], alias: 't2' }
+            ]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT t1.* FROM table1 t1 FULL | JOIN"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT t1.* FROM table1 t1 FULL ',
+        afterCursor: ' JOIN',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['OUTER']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT t1.* FROM table1 t1 RIGHT | JOIN"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT t1.* FROM table1 t1 RIGHT ',
+        afterCursor: ' JOIN',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['OUTER']
+        }
+      });
+    });
+
+    it('should suggest joins for "SELECT * FROM testTable1 |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 ',
+        afterCursor: '',
+        containsKeywords: ['JOIN'],
+        expectedResult: {
+          lowerCase: false,
+          suggestJoins: {
+            prependJoin: true,
+            tables: [{ identifierChain: [{ name: 'testTable1' }] }]
+          },
+          suggestFilters: {
+            prefix: 'WHERE',
+            tables: [{ identifierChain: [{ name: 'testTable1' }] }]
+          },
+          suggestGroupBys: {
+            prefix: 'GROUP BY',
+            tables: [{ identifierChain: [{ name: 'testTable1' }] }]
+          },
+          suggestOrderBys: {
+            prefix: 'ORDER BY',
+            tables: [{ identifierChain: [{ name: 'testTable1' }] }]
+          }
+        }
+      });
+    });
+  });
+
+  describe('SubQueries in WHERE Clause', () => {
+    it('should suggest keywords for "SELECT * FROM foo WHERE bar |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE bar ',
+        afterCursor: '',
+        containsKeywords: ['IN', 'NOT IN'],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'foo' }, { name: 'bar' }] },
+          suggestGroupBys: { prefix: 'GROUP BY', tables: [{ identifierChain: [{ name: 'foo' }] }] },
+          suggestOrderBys: { prefix: 'ORDER BY', tables: [{ identifierChain: [{ name: 'foo' }] }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM foo WHERE bar NOT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE bar NOT ',
+        afterCursor: '',
+        containsKeywords: ['IN'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM foo WHERE bar IN (|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE bar IN (',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['SELECT'],
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'foo' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'foo' }, { name: 'bar' }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "select * from foo, bar where bar.bla in (|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select * from foo, bar where bar.bla in (',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: true,
+          suggestKeywords: ['SELECT'],
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'foo' }] }, { identifierChain: [{ name: 'bar' }] }]
+          },
+          suggestIdentifiers: [{ name: 'foo.', type: 'table' }, { name: 'bar.', type: 'table' }],
+          colRef: { identifierChain: [{ name: 'bar' }, { name: 'bla' }] }
+        }
+      });
+    });
+
+    it('should suggest values for "select * from foo, bar where bar.bla in (\'a\', |"', () => {
+      assertAutoComplete({
+        beforeCursor: "select * from foo, bar where bar.bla in ('a', ",
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: true,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'foo' }] }, { identifierChain: [{ name: 'bar' }] }]
+          },
+          suggestIdentifiers: [{ name: 'foo.', type: 'table' }, { name: 'bar.', type: 'table' }],
+          colRef: { identifierChain: [{ name: 'bar' }, { name: 'bla' }] }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT * FROM foo WHERE bar IN (SELECT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE bar IN (SELECT ',
+        afterCursor: '',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT * FROM bar WHERE foo NOT IN (SELECT |)"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM bar WHERE foo NOT IN (SELECT ',
+        afterCursor: ')',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+  });
+
+  describe('SubQueries in FROM Clause', () => {
+    it('should suggest keywords for "SELECT * FROM (|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM (',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['SELECT']
+        }
+      });
+    });
+
+    it('should suggest keywords for "select * from (|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select * from (',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: true,
+          suggestKeywords: ['SELECT']
+        }
+      });
+    });
+
+    it('should suggest keywords for "select foo.* from (|) foo"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select foo.* from (',
+        afterCursor: ') foo',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: true
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT * FROM (SELECT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM (SELECT ',
+        afterCursor: '',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    // TODO: In this case the WHERE clause exists but isn't completely defined both for the query and the subquery
+    it('should suggest columns for "SELECT "contains an even number" FROM t1, t2 AS ta2 WHERE EXISTS (SELECT t3.foo FROM t3 WHERE | % 2 = 0"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT "contains an even number" FROM t1, t2 AS ta2 WHERE EXISTS (SELECT t3.foo FROM t3 WHERE ',
+        afterCursor: ' % 2 = 0',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          suggestColumns: {
+            types: ['NUMBER'],
+            source: 'where',
+            tables: [
+              { identifierChain: [{ name: 't1' }] },
+              { identifierChain: [{ name: 't2' }], alias: 'ta2' },
+              { identifierChain: [{ name: 't3' }] }
+            ]
+          },
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestIdentifiers: [
+            { name: 't1.', type: 'table' },
+            { name: 'ta2.', type: 'alias' },
+            { name: 't3.', type: 'table' }
+          ],
+          lowerCase: false,
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 103 }
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 33 }
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 39, last_column: 41 },
+              identifierChain: [{ name: 't1' }]
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 43, last_column: 45 },
+              identifierChain: [{ name: 't2' }]
+            },
+            {
+              type: 'alias',
+              source: 'table',
+              alias: 'ta2',
+              location: { first_line: 1, last_line: 1, first_column: 49, last_column: 52 },
+              identifierChain: [{ name: 't2' }]
+            },
+            {
+              type: 'whereClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 52, last_column: 52 }
+            },
+            {
+              type: 'limitClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 52, last_column: 52 }
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 74, last_column: 80 },
+              subquery: true
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 74, last_column: 76 },
+              identifierChain: [{ name: 't3' }]
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 77, last_column: 80 },
+              identifierChain: [{ name: 'foo' }],
+              tables: [{ identifierChain: [{ name: 't3' }] }],
+              qualified: true
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 86, last_column: 88 },
+              identifierChain: [{ name: 't3' }]
+            },
+            {
+              type: 'whereClause',
+              subquery: true,
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 88, last_column: 88 }
+            },
+            {
+              type: 'limitClause',
+              subquery: true,
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 88, last_column: 88 }
+            }
+          ]
+        }
+      });
+    });
+
+    it('should suggest identifiers for "SELECT | FROM testTable tt, (SELECT bla FROM abc WHERE foo > 1) bar"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' FROM testTable tt, (SELECT bla FROM abc WHERE foo > 1) bar',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 67 }
+            },
+            {
+              type: 'selectList',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 7, last_column: 7 }
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 14, last_column: 23 },
+              identifierChain: [{ name: 'testTable' }]
+            },
+            {
+              type: 'alias',
+              source: 'table',
+              alias: 'tt',
+              location: { first_line: 1, last_line: 1, first_column: 24, last_column: 26 },
+              identifierChain: [{ name: 'testTable' }]
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 36, last_column: 39 },
+              subquery: true
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 36, last_column: 39 },
+              identifierChain: [{ name: 'bla' }],
+              tables: [{ identifierChain: [{ name: 'abc' }] }],
+              qualified: false
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 45, last_column: 48 },
+              identifierChain: [{ name: 'abc' }]
+            },
+            {
+              type: 'whereClause',
+              subquery: true,
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 49, last_column: 62 }
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 55, last_column: 58 },
+              identifierChain: [{ name: 'foo' }],
+              tables: [{ identifierChain: [{ name: 'abc' }] }],
+              qualified: false
+            },
+            {
+              type: 'limitClause',
+              subquery: true,
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 62, last_column: 62 }
+            },
+            {
+              type: 'alias',
+              source: 'subquery',
+              alias: 'bar',
+              location: { first_line: 1, last_line: 1, first_column: 64, last_column: 67 }
+            },
+            {
+              type: 'whereClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 67, last_column: 67 }
+            },
+            {
+              type: 'limitClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 67, last_column: 67 }
+            }
+          ],
+          suggestAggregateFunctions: {
+            tables: [{ identifierChain: [{ name: 'testTable' }], alias: 'tt' }]
+          },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [
+              { identifierChain: [{ name: 'testTable' }], alias: 'tt' },
+              { identifierChain: [{ subQuery: 'bar' }] }
+            ]
+          },
+          suggestIdentifiers: [{ name: 'tt.', type: 'alias' }, { name: 'bar.', type: 'sub-query' }],
+          subQueries: [
+            {
+              alias: 'bar',
+              columns: [{ identifierChain: [{ name: 'abc' }, { name: 'bla' }], type: 'COLREF' }]
+            }
+          ],
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest columns for "select | from (select id i, name as n, bla from foo) bar"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select ',
+        afterCursor: ' from (select id i, name as n, bla from foo) bar',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ subQuery: 'bar' }] }]
+          },
+          subQueries: [
+            {
+              alias: 'bar',
+              columns: [
+                { alias: 'i', identifierChain: [{ name: 'foo' }, { name: 'id' }], type: 'COLREF' },
+                {
+                  alias: 'n',
+                  identifierChain: [{ name: 'foo' }, { name: 'name' }],
+                  type: 'COLREF'
+                },
+                { identifierChain: [{ name: 'foo' }, { name: 'bla' }], type: 'COLREF' }
+              ]
+            }
+          ],
+          suggestIdentifiers: [{ name: 'bar.', type: 'sub-query' }],
+          lowerCase: true
+        }
+      });
+    });
+
+    it('should suggest sub-query columns for "SELECT bar.| FROM (SELECT col1, col2, (col3 + 1) col3alias FROM foo) bar"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT bar.',
+        afterCursor: ' FROM (SELECT col1, col2, (col3 + 1) col3alias FROM foo) bar',
+        expectedResult: {
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ subQuery: 'bar' }] }]
+          },
+          suggestKeywords: ['*'],
+          lowerCase: false,
+          subQueries: [
+            {
+              alias: 'bar',
+              columns: [
+                { identifierChain: [{ name: 'foo' }, { name: 'col1' }], type: 'COLREF' },
+                { identifierChain: [{ name: 'foo' }, { name: 'col2' }], type: 'COLREF' },
+                { alias: 'col3alias', type: 'NUMBER' }
+              ]
+            }
+          ]
+        }
+      });
+    });
+
+    it('should suggest sub-query columns for "SELECT bar.| FROM (SELECT b FROM foo) boo, (SELECT a FROM bla) bar"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT bar.',
+        afterCursor: ' FROM (SELECT b FROM foo) boo, (SELECT a FROM bla) bar',
+        expectedResult: {
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ subQuery: 'bar' }] }]
+          },
+          suggestKeywords: ['*'],
+          subQueries: [
+            {
+              alias: 'boo',
+              columns: [{ identifierChain: [{ name: 'foo' }, { name: 'b' }], type: 'COLREF' }]
+            },
+            {
+              alias: 'bar',
+              columns: [{ identifierChain: [{ name: 'bla' }, { name: 'a' }], type: 'COLREF' }]
+            }
+          ],
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest identifiers for "SELECT cos(| FROM (SELECT b FROM foo) boo, (SELECT a FROM bla) bar"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT cos(',
+        afterCursor: ' FROM (SELECT b FROM foo) boo, (SELECT a FROM bla) bar',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          suggestColumns: {
+            source: 'select',
+            types: ['T'],
+            tables: [
+              { identifierChain: [{ subQuery: 'boo' }] },
+              { identifierChain: [{ subQuery: 'bar' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'boo.', type: 'sub-query' },
+            { name: 'bar.', type: 'sub-query' }
+          ],
+          suggestFunctions: { types: ['T'] },
+          subQueries: [
+            {
+              alias: 'boo',
+              columns: [{ identifierChain: [{ name: 'foo' }, { name: 'b' }], type: 'COLREF' }]
+            },
+            {
+              alias: 'bar',
+              columns: [{ identifierChain: [{ name: 'bla' }, { name: 'a' }], type: 'COLREF' }]
+            }
+          ],
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT * FROM (SELECT |)"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM (SELECT ',
+        afterCursor: ')',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM (SELECT | FROM tableOne) subQueryOne, someDb.tableTwo talias, (SELECT * FROM t3 JOIN t4 ON t3.id = t4.id) AS subQueryTwo;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM (SELECT ',
+        afterCursor:
+          ' FROM tableOne) subQueryOne, someDb.tableTwo talias, (SELECT * FROM t3 JOIN t4 ON t3.id = t4.id) AS subQueryTwo;',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'tableOne' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'tableOne' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT | FROM (SELECT * FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' FROM (SELECT * FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ subQuery: 'subQueryTwo' }] }]
+          },
+          suggestIdentifiers: [{ name: 'subQueryTwo.', type: 'sub-query' }],
+          subQueries: [
+            {
+              alias: 'subQueryTwo',
+              columns: [{ tables: [{ identifierChain: [{ subQuery: 'subQueryOne' }] }] }],
+              subQueries: [
+                {
+                  alias: 'subQueryOne',
+                  columns: [{ tables: [{ identifierChain: [{ name: 'tableOne' }] }] }]
+                }
+              ]
+            }
+          ],
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT | FROM (SELECT * FROM (SELECT * FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo) subQueryThree"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor:
+          ' FROM (SELECT * FROM (SELECT * FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo) subQueryThree',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ subQuery: 'subQueryThree' }] }]
+          },
+          suggestIdentifiers: [{ name: 'subQueryThree.', type: 'sub-query' }],
+          subQueries: [
+            {
+              alias: 'subQueryThree',
+              columns: [{ tables: [{ identifierChain: [{ subQuery: 'subQueryTwo' }] }] }],
+              subQueries: [
+                {
+                  alias: 'subQueryTwo',
+                  columns: [{ tables: [{ identifierChain: [{ subQuery: 'subQueryOne' }] }] }],
+                  subQueries: [
+                    {
+                      alias: 'subQueryOne',
+                      columns: [{ tables: [{ identifierChain: [{ name: 'tableOne' }] }] }]
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM (SELECT | FROM (SELECT * FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo) subQueryThree"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM (SELECT ',
+        afterCursor:
+          ' FROM (SELECT * FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo) subQueryThree',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ subQuery: 'subQueryTwo' }] }]
+          },
+          suggestIdentifiers: [{ name: 'subQueryTwo.', type: 'sub-query' }],
+          subQueries: [
+            {
+              alias: 'subQueryTwo',
+              columns: [{ tables: [{ identifierChain: [{ subQuery: 'subQueryOne' }] }] }],
+              subQueries: [
+                {
+                  alias: 'subQueryOne',
+                  columns: [{ tables: [{ identifierChain: [{ name: 'tableOne' }] }] }]
+                }
+              ]
+            }
+          ]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM (SELECT * FROM (SELECT | FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo) subQueryThree"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM (SELECT * FROM (SELECT ',
+        afterCursor: ' FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo) subQueryThree',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ subQuery: 'subQueryOne' }] }]
+          },
+          suggestIdentifiers: [{ name: 'subQueryOne.', type: 'sub-query' }],
+          subQueries: [
+            {
+              alias: 'subQueryOne',
+              columns: [{ tables: [{ identifierChain: [{ name: 'tableOne' }] }] }]
+            }
+          ]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT s2.| FROM (SELECT a, bla FROM (SELECT a, b, abs(1) as bla FROM testTable) s1) s2;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT s2.',
+        afterCursor:
+          ' FROM (SELECT a, bla FROM (SELECT a, b, abs(1) as bla FROM testTable) s1) s2;',
+        expectedResult: {
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ subQuery: 's2' }] }] },
+          suggestKeywords: ['*'],
+          subQueries: [
+            {
+              alias: 's2',
+              columns: [
+                { identifierChain: [{ subQuery: 's1' }, { name: 'a' }], type: 'COLREF' },
+                { identifierChain: [{ subQuery: 's1' }, { name: 'bla' }], type: 'COLREF' }
+              ],
+              subQueries: [
+                {
+                  alias: 's1',
+                  columns: [
+                    { identifierChain: [{ name: 'testTable' }, { name: 'a' }], type: 'COLREF' },
+                    { identifierChain: [{ name: 'testTable' }, { name: 'b' }], type: 'COLREF' },
+                    { alias: 'bla', type: 'T' }
+                  ]
+                }
+              ]
+            }
+          ],
+          lowerCase: false
+        }
+      });
+    });
+  });
+});

+ 50 - 0
desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParser_Set_Spec.js

@@ -0,0 +1,50 @@
+// 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 SqlTestUtils from 'parse/spec/sqlTestUtils';
+import ksqlAutocompleteParser from '../ksqlAutocompleteParser';
+
+describe('ksqlAutocompleteParser.js SET statements', () => {
+  beforeAll(() => {
+    ksqlAutocompleteParser.yy.parseError = function(msg) {
+      throw Error(msg);
+    };
+    jasmine.addMatchers(SqlTestUtils.testDefinitionMatcher);
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = false;
+
+    expect(
+      ksqlAutocompleteParser.parseSql(
+        testDefinition.beforeCursor,
+        testDefinition.afterCursor,
+        debug
+      )
+    ).toEqualDefinition(testDefinition);
+  };
+
+  it('should suggest keywords for "|"', () => {
+    assertAutoComplete({
+      beforeCursor: '',
+      afterCursor: '',
+      containsKeywords: ['SET'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+});

+ 384 - 0
desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParser_Update_Spec.js

@@ -0,0 +1,384 @@
+// 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 SqlTestUtils from 'parse/spec/sqlTestUtils';
+import ksqlAutocompleteParser from '../ksqlAutocompleteParser';
+
+describe('ksqlAutocompleteParser.js UPDATE statements', () => {
+  beforeAll(() => {
+    ksqlAutocompleteParser.yy.parseError = function(msg) {
+      throw Error(msg);
+    };
+    jasmine.addMatchers(SqlTestUtils.testDefinitionMatcher);
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = false;
+
+    expect(
+      ksqlAutocompleteParser.parseSql(
+        testDefinition.beforeCursor,
+        testDefinition.afterCursor,
+        debug
+      )
+    ).toEqualDefinition(testDefinition);
+  };
+
+  it('should suggest keywords for "|"', () => {
+    assertAutoComplete({
+      beforeCursor: '',
+      afterCursor: '',
+      containsKeywords: ['UPDATE'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest keywords for "UPDATE bar  |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'UPDATE bar  ',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestKeywords: ['SET'],
+        locations: [
+          {
+            type: 'statement',
+            location: { first_line: 1, last_line: 1, first_column: 1, last_column: 12 }
+          },
+          {
+            type: 'table',
+            location: { first_line: 1, last_line: 1, first_column: 8, last_column: 11 },
+            identifierChain: [{ name: 'bar' }]
+          }
+        ]
+      }
+    });
+  });
+
+  it('should suggest keywords for "UPDATE bar SET id=1, foo=2 |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'UPDATE bar SET id=1, foo=2 ',
+      afterCursor: '',
+      containsKeywords: ['WHERE'],
+      expectedResult: {
+        lowerCase: false,
+        locations: [
+          {
+            type: 'statement',
+            location: { first_line: 1, last_line: 1, first_column: 1, last_column: 27 }
+          },
+          {
+            type: 'table',
+            location: { first_line: 1, last_line: 1, first_column: 8, last_column: 11 },
+            identifierChain: [{ name: 'bar' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 16, last_column: 18 },
+            identifierChain: [{ name: 'id' }],
+            tables: [{ identifierChain: [{ name: 'bar' }] }],
+            qualified: false
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 22, last_column: 25 },
+            identifierChain: [{ name: 'foo' }],
+            tables: [{ identifierChain: [{ name: 'bar' }] }],
+            qualified: false
+          }
+        ]
+      }
+    });
+  });
+
+  it('should suggest keywords for "UPDATE bar SET id |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'UPDATE bar SET id ',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestKeywords: ['='],
+        locations: [
+          {
+            type: 'statement',
+            location: { first_line: 1, last_line: 1, first_column: 1, last_column: 18 }
+          },
+          {
+            type: 'table',
+            location: { first_line: 1, last_line: 1, first_column: 8, last_column: 11 },
+            identifierChain: [{ name: 'bar' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 16, last_column: 18 },
+            identifierChain: [{ name: 'id' }],
+            tables: [{ identifierChain: [{ name: 'bar' }] }],
+            qualified: false
+          }
+        ]
+      }
+    });
+  });
+
+  it('should suggest tables for "UPDATE |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'UPDATE ',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: {},
+        suggestDatabases: {
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should suggest tables for "UPDATE bla|"', () => {
+    assertAutoComplete({
+      beforeCursor: 'UPDATE bla',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: {},
+        suggestDatabases: {
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should suggest tables for "UPDATE bar.|"', () => {
+    assertAutoComplete({
+      beforeCursor: 'UPDATE bar.',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: { identifierChain: [{ name: 'bar' }] }
+      }
+    });
+  });
+
+  it('should suggest tables for "UPDATE bar.foo|"', () => {
+    assertAutoComplete({
+      beforeCursor: 'UPDATE bar.foo',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: { identifierChain: [{ name: 'bar' }] }
+      }
+    });
+  });
+
+  it('should suggest columns for "UPDATE bar.foo SET |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'UPDATE bar.foo SET ',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestColumns: { tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }] },
+        locations: [
+          {
+            type: 'statement',
+            location: { first_line: 1, last_line: 1, first_column: 1, last_column: 19 }
+          },
+          {
+            type: 'database',
+            location: { first_line: 1, last_line: 1, first_column: 8, last_column: 11 },
+            identifierChain: [{ name: 'bar' }]
+          },
+          {
+            type: 'table',
+            location: { first_line: 1, last_line: 1, first_column: 12, last_column: 15 },
+            identifierChain: [{ name: 'bar' }, { name: 'foo' }]
+          }
+        ]
+      }
+    });
+  });
+
+  it('should suggest columns for "UPDATE bar.foo SET id = 1, bla = \'foo\', |"', () => {
+    assertAutoComplete({
+      beforeCursor: "UPDATE bar.foo SET id = 1, bla = 'foo', ",
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestColumns: { tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }] },
+        locations: [
+          {
+            type: 'statement',
+            location: { first_line: 1, last_line: 1, first_column: 1, last_column: 40 }
+          },
+          {
+            type: 'database',
+            location: { first_line: 1, last_line: 1, first_column: 8, last_column: 11 },
+            identifierChain: [{ name: 'bar' }]
+          },
+          {
+            type: 'table',
+            location: { first_line: 1, last_line: 1, first_column: 12, last_column: 15 },
+            identifierChain: [{ name: 'bar' }, { name: 'foo' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 20, last_column: 22 },
+            identifierChain: [{ name: 'id' }],
+            tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }],
+            qualified: false
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 28, last_column: 31 },
+            identifierChain: [{ name: 'bla' }],
+            tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }],
+            qualified: false
+          }
+        ]
+      }
+    });
+  });
+
+  it('should suggest columns for "UPDATE bar.foo SET bla = \'foo\' WHERE |"', () => {
+    assertAutoComplete({
+      beforeCursor: "UPDATE bar.foo SET bla = 'foo' WHERE ",
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestFunctions: {},
+        suggestColumns: { tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }] },
+        suggestFilters: { tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }] },
+        suggestKeywords: ['EXISTS', 'NOT EXISTS'],
+        locations: [
+          {
+            type: 'statement',
+            location: { first_line: 1, last_line: 1, first_column: 1, last_column: 37 }
+          },
+          {
+            type: 'database',
+            location: { first_line: 1, last_line: 1, first_column: 8, last_column: 11 },
+            identifierChain: [{ name: 'bar' }]
+          },
+          {
+            type: 'table',
+            location: { first_line: 1, last_line: 1, first_column: 12, last_column: 15 },
+            identifierChain: [{ name: 'bar' }, { name: 'foo' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 20, last_column: 23 },
+            identifierChain: [{ name: 'bla' }],
+            tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }],
+            qualified: false
+          }
+        ]
+      }
+    });
+  });
+
+  it('should suggest values for "UPDATE bar.foo SET bla = \'foo\' WHERE id = |"', () => {
+    assertAutoComplete({
+      beforeCursor: "UPDATE bar.foo SET bla = 'foo' WHERE id = ",
+      afterCursor: '',
+      containsKeywords: ['CASE'],
+      expectedResult: {
+        locations: [
+          {
+            type: 'statement',
+            location: { first_line: 1, last_line: 1, first_column: 1, last_column: 42 }
+          },
+          {
+            type: 'database',
+            location: { first_line: 1, last_line: 1, first_column: 8, last_column: 11 },
+            identifierChain: [{ name: 'bar' }]
+          },
+          {
+            type: 'table',
+            location: { first_line: 1, last_line: 1, first_column: 12, last_column: 15 },
+            identifierChain: [{ name: 'bar' }, { name: 'foo' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 20, last_column: 23 },
+            identifierChain: [{ name: 'bla' }],
+            tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }],
+            qualified: false
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 38, last_column: 40 },
+            identifierChain: [{ name: 'id' }],
+            tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }],
+            qualified: false
+          }
+        ],
+        suggestFunctions: { types: ['COLREF'] },
+        suggestValues: {},
+        colRef: { identifierChain: [{ name: 'bar' }, { name: 'foo' }, { name: 'id' }] },
+        suggestColumns: {
+          types: ['COLREF'],
+          tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }]
+        },
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest columns for "UPDATE bar.foo SET bla = \'foo\' WHERE id = 1 AND |"', () => {
+    assertAutoComplete({
+      beforeCursor: "UPDATE bar.foo SET bla = 'foo' WHERE id = 1 AND ",
+      afterCursor: '',
+      containsKeywords: ['CASE'],
+      expectedResult: {
+        lowerCase: false,
+        suggestFunctions: {},
+        suggestColumns: { tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }] },
+        suggestFilters: { tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }] },
+        locations: [
+          {
+            type: 'statement',
+            location: { first_line: 1, last_line: 1, first_column: 1, last_column: 48 }
+          },
+          {
+            type: 'database',
+            location: { first_line: 1, last_line: 1, first_column: 8, last_column: 11 },
+            identifierChain: [{ name: 'bar' }]
+          },
+          {
+            type: 'table',
+            location: { first_line: 1, last_line: 1, first_column: 12, last_column: 15 },
+            identifierChain: [{ name: 'bar' }, { name: 'foo' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 20, last_column: 23 },
+            identifierChain: [{ name: 'bla' }],
+            tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }],
+            qualified: false
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 38, last_column: 40 },
+            identifierChain: [{ name: 'id' }],
+            tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }],
+            qualified: false
+          }
+        ]
+      }
+    });
+  });
+});

+ 146 - 0
desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParser_Use_Spec.js

@@ -0,0 +1,146 @@
+// 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 SqlTestUtils from 'parse/spec/sqlTestUtils';
+import ksqlAutocompleteParser from '../ksqlAutocompleteParser';
+
+describe('ksqlAutocompleteParser.js USE statements', () => {
+  beforeAll(() => {
+    ksqlAutocompleteParser.yy.parseError = function(msg) {
+      throw Error(msg);
+    };
+    jasmine.addMatchers(SqlTestUtils.testDefinitionMatcher);
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = false;
+
+    expect(
+      ksqlAutocompleteParser.parseSql(
+        testDefinition.beforeCursor,
+        testDefinition.afterCursor,
+        debug
+      )
+    ).toEqualDefinition(testDefinition);
+  };
+
+  it('should suggest keywords for "|"', () => {
+    assertAutoComplete({
+      beforeCursor: '',
+      afterCursor: '',
+      containsKeywords: ['USE'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest databases for "USE |"', () => {
+    assertAutoComplete({
+      serverResponses: {},
+      beforeCursor: 'USE ',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestDatabases: {}
+      }
+    });
+  });
+
+  it('should suggest databases for "USE bla|"', () => {
+    assertAutoComplete({
+      serverResponses: {},
+      beforeCursor: 'USE bla',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestDatabases: {}
+      }
+    });
+  });
+
+  it('should use a use statement for "use database_two; \\nselect |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'use database_two; \nSELECT ',
+      afterCursor: '',
+      containsKeywords: ['*', 'ALL', 'DISTINCT'],
+      expectedResult: {
+        useDatabase: 'database_two',
+        lowerCase: true,
+        suggestAggregateFunctions: { tables: [] },
+        suggestAnalyticFunctions: true,
+        suggestFunctions: {},
+        suggestTables: {
+          prependQuestionMark: true,
+          prependFrom: true
+        },
+        suggestDatabases: {
+          prependQuestionMark: true,
+          prependFrom: true,
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should use the last use statement for "USE other_db; USE closest_db; \\n\\tSELECT |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'USE other_db; USE closest_db; \n\tSELECT ',
+      afterCursor: '',
+      containsKeywords: ['*', 'ALL', 'DISTINCT'],
+      expectedResult: {
+        useDatabase: 'closest_db',
+        lowerCase: false,
+        suggestAggregateFunctions: { tables: [] },
+        suggestAnalyticFunctions: true,
+        suggestFunctions: {},
+        suggestTables: {
+          prependQuestionMark: true,
+          prependFrom: true
+        },
+        suggestDatabases: {
+          prependQuestionMark: true,
+          prependFrom: true,
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should use the use statement for "USE other_db; USE closest_db; \\n\\tSELECT |; USE some_other_db;"', () => {
+    assertAutoComplete({
+      beforeCursor: 'USE other_db; USE closest_db; \n\tSELECT ',
+      afterCursor: '; USE some_other_db;',
+      containsKeywords: ['*', 'ALL', 'DISTINCT'],
+      expectedResult: {
+        useDatabase: 'closest_db',
+        lowerCase: false,
+        suggestAggregateFunctions: { tables: [] },
+        suggestAnalyticFunctions: true,
+        suggestFunctions: {},
+        suggestTables: {
+          prependQuestionMark: true,
+          prependFrom: true
+        },
+        suggestDatabases: {
+          prependQuestionMark: true,
+          prependFrom: true,
+          appendDot: true
+        }
+      }
+    });
+  });
+});

+ 208 - 0
desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlSyntaxParserSpec.js

@@ -0,0 +1,208 @@
+// 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 ksqlSyntaxParser from '../ksqlSyntaxParser';
+
+describe('ksqlSyntaxParser.js', () => {
+  const expectedToStrings = function(expected) {
+    return expected.map(ex => ex.text);
+  };
+
+  it('should not find errors for ""', () => {
+    const result = ksqlSyntaxParser.parseSyntax('', '');
+
+    expect(result).toBeFalsy();
+  });
+
+  it('should report incomplete statement for "SEL"', () => {
+    const result = ksqlSyntaxParser.parseSyntax('SEL', '');
+
+    expect(result.incompleteStatement).toBeTruthy();
+  });
+
+  it('should report incomplete statement for "SELECT"', () => {
+    const result = ksqlSyntaxParser.parseSyntax('SELECT', '');
+
+    expect(result.incompleteStatement).toBeTruthy();
+  });
+
+  it('should report incomplete statement for "SELECT "', () => {
+    const result = ksqlSyntaxParser.parseSyntax('SELECT ', '');
+
+    expect(result.incompleteStatement).toBeTruthy();
+  });
+
+  it('should not report incomplete statement for "SELECT * FROM tbl"', () => {
+    const result = ksqlSyntaxParser.parseSyntax('SELECT * FROM tbl', '');
+
+    expect(result.incompleteStatement).toBeFalsy();
+  });
+
+  it('should not report incomplete statement for "SELECT * FROM tbl LIMIT 1"', () => {
+    const result = ksqlSyntaxParser.parseSyntax('SELECT * FROM tbl LIMIT 1', '');
+
+    expect(result.incompleteStatement).toBeFalsy();
+  });
+
+  it('should report incomplete statement for "SELECT * FROM tbl LIMIT "', () => {
+    const result = ksqlSyntaxParser.parseSyntax('SELECT * FROM tbl LIMIT ', '');
+
+    expect(result.incompleteStatement).toBeTruthy();
+  });
+
+  it('should report incomplete statement for "SELECT * FROM tbl GROUP"', () => {
+    const result = ksqlSyntaxParser.parseSyntax('SELECT * FROM tbl GROUP', '');
+
+    expect(result.incompleteStatement).toBeTruthy();
+  });
+
+  it('should not find errors for "SELECT *"', () => {
+    const result = ksqlSyntaxParser.parseSyntax('SELECT *', '');
+
+    expect(result).toBeFalsy();
+  });
+
+  it('should not report incomplete statement for "SELECT * FR"', () => {
+    const result = ksqlSyntaxParser.parseSyntax('SELECT * FR', '');
+
+    expect(result.incompleteStatement).toBeTruthy();
+  });
+
+  it('should find errors for "SLELECT "', () => {
+    const result = ksqlSyntaxParser.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);
+  });
+
+  it('should find errors for "alter tabel "', () => {
+    const result = ksqlSyntaxParser.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 = ksqlSyntaxParser.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 = ksqlSyntaxParser.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 = ksqlSyntaxParser.parseSyntax('select asdf wer qwer qewr   qwer', '');
+
+    expect(result).toBeTruthy();
+  });
+
+  it('should suggest expected words for "SLELECT "', () => {
+    const result = ksqlSyntaxParser.parseSyntax('SLELECT ', '');
+
+    expect(result).toBeTruthy();
+    expect(expectedToStrings(result.expected)).toEqual([
+      'SELECT',
+      'SET',
+      'ALTER',
+      'INSERT',
+      'CREATE',
+      'USE',
+      'DROP',
+      'TRUNCATE',
+      'UPDATE',
+      'WITH'
+    ]);
+  });
+
+  it('should suggest expected words for "slelect "', () => {
+    const result = ksqlSyntaxParser.parseSyntax('slelect ', '');
+
+    expect(result).toBeTruthy();
+    expect(expectedToStrings(result.expected)).toEqual([
+      'select',
+      'set',
+      'alter',
+      'insert',
+      'create',
+      'use',
+      'drop',
+      'truncate',
+      'update',
+      'with'
+    ]);
+  });
+
+  it('should suggest expected that the statement should end for "use somedb extrastuff "', () => {
+    const result = ksqlSyntaxParser.parseSyntax('use somedb extrastuff  ', '');
+
+    expect(result).toBeTruthy();
+    expect(result.expectedStatementEnd).toBeTruthy();
+  });
+
+  const expectEqualIds = function(beforeA, afterA, beforeB, afterB) {
+    const resultA = ksqlSyntaxParser.parseSyntax(beforeA, afterA);
+    const resultB = ksqlSyntaxParser.parseSyntax(beforeB, afterB);
+
+    expect(resultA).toBeTruthy('"' + beforeA + '|' + afterA + '" was not reported as an error');
+    expect(resultB).toBeTruthy('"' + beforeB + '|' + afterB + '" was not reported as an error');
+    expect(resultA.ruleId).toEqual(resultB.ruleId);
+  };
+
+  const expectNonEqualIds = function(beforeA, afterA, beforeB, afterB) {
+    const resultA = ksqlSyntaxParser.parseSyntax(beforeA, afterA);
+    const resultB = ksqlSyntaxParser.parseSyntax(beforeB, afterB);
+
+    expect(resultA).toBeTruthy('"' + beforeA + '|' + afterA + '" was not reported as an error');
+    expect(resultB).toBeTruthy('"' + beforeB + '|' + afterB + '" was not reported as an error');
+    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 ', '');
+  });
+});

+ 2221 - 0
desktop/core/src/desktop/js/parse/sql/ksql/sqlParseSupport.js

@@ -0,0 +1,2221 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { SqlFunctions } from 'sql/sqlFunctions';
+import stringDistance from 'sql/stringDistance';
+
+const identifierEquals = (a, b) =>
+  a &&
+  b &&
+  a
+    .replace(/^\s*`/, '')
+    .replace(/`\s*$/, '')
+    .toLowerCase() ===
+    b
+      .replace(/^\s*`/, '')
+      .replace(/`\s*$/, '')
+      .toLowerCase();
+
+// endsWith polyfill from hue_utils.js, needed as workers live in their own js environment
+if (!String.prototype.endsWith) {
+  String.prototype.endsWith = function(searchString, position) {
+    const subjectString = this.toString();
+    if (
+      typeof position !== 'number' ||
+      !isFinite(position) ||
+      Math.floor(position) !== position ||
+      position > subjectString.length
+    ) {
+      position = subjectString.length;
+    }
+    position -= searchString.length;
+    const lastIndex = subjectString.lastIndexOf(searchString, position);
+    return lastIndex !== -1 && lastIndex === position;
+  };
+}
+
+const equalIgnoreCase = (a, b) => a && b && a.toLowerCase() === b.toLowerCase();
+
+const SIMPLE_TABLE_REF_SUGGESTIONS = [
+  'suggestJoinConditions',
+  'suggestAggregateFunctions',
+  'suggestFilters',
+  'suggestGroupBys',
+  'suggestOrderBys'
+];
+
+const initSqlParser = function(parser) {
+  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 (
+      typeof SqlFunctions === 'undefined' ||
+      SqlFunctions.matchesType(parser.yy.activeDialect, ['BOOLEAN'], types)
+    ) {
+      keywords = keywords.concat(['AND', 'OR']);
+    }
+    if (
+      typeof SqlFunctions === 'undefined' ||
+      SqlFunctions.matchesType(parser.yy.activeDialect, ['NUMBER'], types)
+    ) {
+      keywords = keywords.concat(['+', '-', '*', '/', '%', 'DIV']);
+    }
+    if (
+      typeof SqlFunctions === 'undefined' ||
+      SqlFunctions.matchesType(parser.yy.activeDialect, ['STRING'], types)
+    ) {
+      keywords = keywords.concat(['LIKE', 'NOT LIKE', 'REGEXP', 'RLIKE']);
+    }
+    return { suggestKeywords: keywords };
+  };
+
+  parser.getTypeKeywords = function() {
+    return [
+      'BIGINT',
+      'BOOLEAN',
+      'CHAR',
+      'DECIMAL',
+      'DOUBLE',
+      'FLOAT',
+      'INT',
+      'SMALLINT',
+      'TIMESTAMP',
+      'STRING',
+      'TINYINT',
+      'VARCHAR'
+    ];
+  };
+
+  parser.getColumnDataTypeKeywords = function() {
+    return parser.getTypeKeywords();
+  };
+
+  parser.addColRefIfExists = function(valueExpression) {
+    if (valueExpression.columnReference) {
+      parser.yy.result.colRef = { identifierChain: valueExpression.columnReference };
+    }
+  };
+
+  parser.selectListNoTableSuggest = function(selectListEdit, hasDistinctOrAll) {
+    if (selectListEdit.cursorAtStart) {
+      let keywords = parser.getSelectListKeywords();
+      if (!hasDistinctOrAll) {
+        keywords = keywords.concat([{ value: 'ALL', weight: 2 }, { value: 'DISTINCT', weight: 2 }]);
+      }
+      parser.suggestKeywords(keywords);
+    } else {
+      parser.checkForKeywords(selectListEdit);
+    }
+    if (selectListEdit.suggestFunctions) {
+      parser.suggestFunctions();
+    }
+    if (selectListEdit.suggestColumns) {
+      parser.suggestColumns();
+    }
+    if (
+      selectListEdit.suggestAggregateFunctions &&
+      (!hasDistinctOrAll || hasDistinctOrAll === 'ALL')
+    ) {
+      parser.suggestAggregateFunctions();
+      parser.suggestAnalyticFunctions();
+    }
+  };
+
+  parser.suggestJoinConditions = function(details) {
+    parser.yy.result.suggestJoinConditions = details || {};
+    if (parser.yy.latestTablePrimaries && !parser.yy.result.suggestJoinConditions.tablePrimaries) {
+      parser.yy.result.suggestJoinConditions.tablePrimaries = parser.yy.latestTablePrimaries.concat();
+    }
+  };
+
+  parser.suggestJoins = function(details) {
+    parser.yy.result.suggestJoins = details || {};
+  };
+
+  parser.valueExpressionSuggest = function(oppositeValueExpression, operator) {
+    if (oppositeValueExpression && oppositeValueExpression.columnReference) {
+      parser.suggestValues();
+      parser.yy.result.colRef = { identifierChain: oppositeValueExpression.columnReference };
+    }
+    parser.suggestColumns();
+    parser.suggestFunctions();
+    let keywords = [
+      { value: 'CASE', weight: 450 },
+      { value: 'FALSE', weight: 450 },
+      { value: 'NULL', weight: 450 },
+      { value: 'TRUE', weight: 450 }
+    ];
+    if (typeof oppositeValueExpression === 'undefined' || typeof operator === 'undefined') {
+      keywords = keywords.concat(['EXISTS', 'NOT']);
+    }
+    if (oppositeValueExpression && oppositeValueExpression.types[0] === 'NUMBER') {
+      parser.applyTypeToSuggestions(['NUMBER']);
+    }
+    parser.suggestKeywords(keywords);
+  };
+
+  parser.applyTypeToSuggestions = function(types) {
+    if (types[0] === 'BOOLEAN') {
+      return;
+    }
+    if (parser.yy.result.suggestFunctions && !parser.yy.result.suggestFunctions.types) {
+      parser.yy.result.suggestFunctions.types = types;
+    }
+    if (parser.yy.result.suggestColumns && !parser.yy.result.suggestColumns.types) {
+      parser.yy.result.suggestColumns.types = types;
+    }
+  };
+
+  parser.findCaseType = function(whenThenList) {
+    const types = {};
+    whenThenList.caseTypes.forEach(valueExpression => {
+      valueExpression.types.forEach(type => {
+        types[type] = true;
+      });
+    });
+    if (Object.keys(types).length === 1) {
+      return { types: [Object.keys(types)[0]] };
+    }
+    return { types: ['T'] };
+  };
+
+  parser.findReturnTypes = function(functionName) {
+    return typeof SqlFunctions === 'undefined'
+      ? ['T']
+      : SqlFunctions.getReturnTypes(parser.yy.activeDialect, functionName.toLowerCase());
+  };
+
+  parser.applyArgumentTypesToSuggestions = function(functionName, position) {
+    const foundArguments =
+      typeof SqlFunctions === 'undefined'
+        ? ['T']
+        : SqlFunctions.getArgumentTypes(
+            parser.yy.activeDialect,
+            functionName.toLowerCase(),
+            position
+          );
+    if (foundArguments.length === 0 && parser.yy.result.suggestColumns) {
+      delete parser.yy.result.suggestColumns;
+      delete parser.yy.result.suggestKeyValues;
+      delete parser.yy.result.suggestValues;
+      delete parser.yy.result.suggestFunctions;
+      delete parser.yy.result.suggestIdentifiers;
+      delete parser.yy.result.suggestKeywords;
+    } else {
+      parser.applyTypeToSuggestions(foundArguments);
+    }
+  };
+
+  parser.commitLocations = function() {
+    if (parser.yy.locations.length === 0) {
+      return;
+    }
+
+    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;
+      }
+    }
+  };
+
+  parser.identifyPartials = function(beforeCursor, afterCursor) {
+    const beforeMatch = beforeCursor.match(/[0-9a-zA-Z_]*$/);
+    const afterMatch = afterCursor.match(/^[0-9a-zA-Z_]*(?:\((?:[^)]*\))?)?/);
+    return {
+      left: beforeMatch ? beforeMatch[0].length : 0,
+      right: afterMatch ? afterMatch[0].length : 0
+    };
+  };
+
+  const addCleanTablePrimary = function(tables, tablePrimary) {
+    if (tablePrimary.alias) {
+      tables.push({ alias: tablePrimary.alias, identifierChain: tablePrimary.identifierChain });
+    } else {
+      tables.push({ identifierChain: tablePrimary.identifierChain });
+    }
+  };
+
+  parser.expandIdentifierChain = function(options) {
+    const wrapper = options.wrapper;
+    const anyOwner = options.anyOwner;
+    const isColumnWrapper = options.isColumnWrapper;
+    const isColumnLocation = options.isColumnLocation;
+    let tablePrimaries = options.tablePrimaries || parser.yy.latestTablePrimaries;
+
+    if (typeof wrapper.identifierChain === 'undefined' || typeof tablePrimaries === 'undefined') {
+      return;
+    }
+    let identifierChain = wrapper.identifierChain.concat();
+
+    if (tablePrimaries.length === 0) {
+      delete wrapper.identifierChain;
+      return;
+    }
+
+    if (!anyOwner) {
+      tablePrimaries = filterTablePrimariesForOwner(tablePrimaries, wrapper.owner);
+    }
+
+    if (identifierChain.length > 0 && identifierChain[identifierChain.length - 1].asterisk) {
+      const tables = [];
+      tablePrimaries.forEach(tablePrimary => {
+        if (identifierChain.length > 1 && !tablePrimary.subQueryAlias) {
+          if (
+            identifierChain.length === 2 &&
+            equalIgnoreCase(tablePrimary.alias, identifierChain[0].name)
+          ) {
+            addCleanTablePrimary(tables, tablePrimary);
+          } else if (
+            identifierChain.length === 2 &&
+            equalIgnoreCase(tablePrimary.identifierChain[0].name, identifierChain[0].name)
+          ) {
+            addCleanTablePrimary(tables, tablePrimary);
+          } else if (
+            identifierChain.length === 3 &&
+            tablePrimary.identifierChain.length > 1 &&
+            equalIgnoreCase(tablePrimary.identifierChain[0].name, identifierChain[0].name) &&
+            equalIgnoreCase(tablePrimary.identifierChain[1].name, identifierChain[1].name)
+          ) {
+            addCleanTablePrimary(tables, tablePrimary);
+          }
+        } else if (tablePrimary.subQueryAlias) {
+          tables.push({ identifierChain: [{ subQuery: tablePrimary.subQueryAlias }] });
+        } else {
+          addCleanTablePrimary(tables, tablePrimary);
+        }
+      });
+      // Possible Joins
+      if (tables.length > 0) {
+        wrapper.tables = tables;
+        delete wrapper.identifierChain;
+        return;
+      }
+    }
+
+    // IdentifierChain contains a possibly started identifier or empty, example: a.b.c = ['a', 'b', 'c']
+    // Reduce the tablePrimaries to the one that matches the first identifier if found
+    let foundPrimary;
+    let doubleMatch = false;
+    let aliasMatch = false;
+    if (identifierChain.length > 0) {
+      for (let i = 0; i < tablePrimaries.length; i++) {
+        if (tablePrimaries[i].subQueryAlias) {
+          if (equalIgnoreCase(tablePrimaries[i].subQueryAlias, identifierChain[0].name)) {
+            foundPrimary = tablePrimaries[i];
+          }
+        } else if (equalIgnoreCase(tablePrimaries[i].alias, identifierChain[0].name)) {
+          foundPrimary = tablePrimaries[i];
+          aliasMatch = true;
+          break;
+        } else if (
+          tablePrimaries[i].identifierChain.length > 1 &&
+          identifierChain.length > 1 &&
+          equalIgnoreCase(tablePrimaries[i].identifierChain[0].name, identifierChain[0].name) &&
+          equalIgnoreCase(tablePrimaries[i].identifierChain[1].name, identifierChain[1].name)
+        ) {
+          foundPrimary = tablePrimaries[i];
+          doubleMatch = true;
+          break;
+        } else if (
+          !foundPrimary &&
+          equalIgnoreCase(tablePrimaries[i].identifierChain[0].name, identifierChain[0].name) &&
+          identifierChain.length > (isColumnLocation ? 1 : 0)
+        ) {
+          foundPrimary = tablePrimaries[i];
+          // No break as first two can still match.
+        } else if (
+          !foundPrimary &&
+          tablePrimaries[i].identifierChain.length > 1 &&
+          !tablePrimaries[i].alias &&
+          equalIgnoreCase(
+            tablePrimaries[i].identifierChain[tablePrimaries[i].identifierChain.length - 1].name,
+            identifierChain[0].name
+          )
+        ) {
+          // This is for the case SELECT baa. FROM bla.baa, blo.boo;
+          foundPrimary = tablePrimaries[i];
+          break;
+        }
+      }
+    }
+
+    if (foundPrimary) {
+      identifierChain.shift();
+      if (doubleMatch) {
+        identifierChain.shift();
+      }
+    } else if (tablePrimaries.length === 1 && !isColumnWrapper) {
+      foundPrimary = tablePrimaries[0];
+    }
+
+    if (foundPrimary) {
+      if (isColumnWrapper) {
+        wrapper.identifierChain = identifierChain;
+        if (foundPrimary.subQueryAlias) {
+          wrapper.tables = [{ subQuery: foundPrimary.subQueryAlias }];
+        } else if (foundPrimary.alias) {
+          if (!isColumnLocation && isColumnWrapper && aliasMatch) {
+            // TODO: add alias on table in suggestColumns (needs support in sqlAutocomplete3.js)
+            // the case is: SELECT cu.| FROM customers cu;
+            // This prevents alias from being added automatically in sqlAutocompleter.js
+            wrapper.tables = [{ identifierChain: foundPrimary.identifierChain }];
+          } else {
+            wrapper.tables = [
+              { identifierChain: foundPrimary.identifierChain, alias: foundPrimary.alias }
+            ];
+          }
+        } else {
+          wrapper.tables = [{ identifierChain: foundPrimary.identifierChain }];
+        }
+      } else {
+        if (foundPrimary.subQueryAlias) {
+          identifierChain.unshift({ subQuery: foundPrimary.subQueryAlias });
+        } else {
+          identifierChain = foundPrimary.identifierChain.concat(identifierChain);
+        }
+        if (wrapper.tables) {
+          wrapper.tables.push({ identifierChain: identifierChain });
+          delete wrapper.identifierChain;
+        } else {
+          wrapper.identifierChain = identifierChain;
+        }
+      }
+    } else {
+      if (isColumnWrapper) {
+        wrapper.tables = [];
+      }
+      tablePrimaries.forEach(tablePrimary => {
+        const targetTable = tablePrimary.subQueryAlias
+          ? { subQuery: tablePrimary.subQueryAlias }
+          : { identifierChain: tablePrimary.identifierChain };
+        if (tablePrimary.alias) {
+          targetTable.alias = tablePrimary.alias;
+        }
+        if (wrapper.tables) {
+          wrapper.tables.push(targetTable);
+        }
+      });
+    }
+    delete wrapper.owner;
+    wrapper.linked = true;
+  };
+
+  const filterTablePrimariesForOwner = function(tablePrimaries, owner) {
+    const result = [];
+    tablePrimaries.forEach(primary => {
+      if (typeof owner === 'undefined' && typeof primary.owner === 'undefined') {
+        result.push(primary);
+      } else if (owner === primary.owner) {
+        result.push(primary);
+      }
+    });
+    return result;
+  };
+
+  const convertTablePrimariesToSuggestions = function(tablePrimaries) {
+    const tables = [];
+    const identifiers = [];
+    tablePrimaries.forEach(tablePrimary => {
+      if (tablePrimary.identifierChain && tablePrimary.identifierChain.length > 0) {
+        const table = { identifierChain: tablePrimary.identifierChain };
+        if (tablePrimary.alias) {
+          table.alias = tablePrimary.alias;
+          identifiers.push({ name: table.alias + '.', type: 'alias' });
+        } else {
+          const lastIdentifier =
+            tablePrimary.identifierChain[tablePrimary.identifierChain.length - 1];
+          if (typeof lastIdentifier.name !== 'undefined') {
+            identifiers.push({ name: lastIdentifier.name + '.', type: 'table' });
+          } else if (typeof lastIdentifier.subQuery !== 'undefined') {
+            identifiers.push({ name: lastIdentifier.subQuery + '.', type: 'sub-query' });
+          }
+        }
+        tables.push(table);
+      } else if (tablePrimary.subQueryAlias) {
+        identifiers.push({ name: tablePrimary.subQueryAlias + '.', type: 'sub-query' });
+        tables.push({ identifierChain: [{ subQuery: tablePrimary.subQueryAlias }] });
+      }
+    });
+    if (identifiers.length > 0) {
+      if (typeof parser.yy.result.suggestIdentifiers === 'undefined') {
+        parser.yy.result.suggestIdentifiers = identifiers;
+      } else {
+        parser.yy.result.suggestIdentifiers = identifiers.concat(
+          parser.yy.result.suggestIdentifiers
+        );
+      }
+    }
+    parser.yy.result.suggestColumns.tables = tables;
+    if (
+      parser.yy.result.suggestColumns.identifierChain &&
+      parser.yy.result.suggestColumns.identifierChain.length === 0
+    ) {
+      delete parser.yy.result.suggestColumns.identifierChain;
+    }
+    parser.yy.result.suggestColumns.linked = true;
+  };
+
+  const linkTablePrimaries = function() {
+    if (!parser.yy.cursorFound || typeof parser.yy.latestTablePrimaries === 'undefined') {
+      return;
+    }
+
+    SIMPLE_TABLE_REF_SUGGESTIONS.forEach(suggestionType => {
+      if (
+        typeof parser.yy.result[suggestionType] !== 'undefined' &&
+        parser.yy.result[suggestionType].tablePrimaries &&
+        !parser.yy.result[suggestionType].linked
+      ) {
+        parser.yy.result[suggestionType].tables = [];
+        parser.yy.result[suggestionType].tablePrimaries.forEach(tablePrimary => {
+          if (!tablePrimary.subQueryAlias) {
+            parser.yy.result[suggestionType].tables.push(
+              tablePrimary.alias
+                ? {
+                    identifierChain: tablePrimary.identifierChain.concat(),
+                    alias: tablePrimary.alias
+                  }
+                : { identifierChain: tablePrimary.identifierChain.concat() }
+            );
+          }
+        });
+        delete parser.yy.result[suggestionType].tablePrimaries;
+        parser.yy.result[suggestionType].linked = true;
+      }
+    });
+
+    if (
+      typeof parser.yy.result.suggestColumns !== 'undefined' &&
+      !parser.yy.result.suggestColumns.linked
+    ) {
+      const tablePrimaries = filterTablePrimariesForOwner(
+        parser.yy.latestTablePrimaries,
+        parser.yy.result.suggestColumns.owner
+      );
+      if (!parser.yy.result.suggestColumns.tables) {
+        parser.yy.result.suggestColumns.tables = [];
+      }
+      if (parser.yy.subQueries.length > 0) {
+        parser.yy.result.subQueries = parser.yy.subQueries;
+      }
+      if (
+        typeof parser.yy.result.suggestColumns.identifierChain === 'undefined' ||
+        parser.yy.result.suggestColumns.identifierChain.length === 0
+      ) {
+        if (tablePrimaries.length > 1) {
+          convertTablePrimariesToSuggestions(tablePrimaries);
+        } else {
+          if (
+            tablePrimaries.length === 1 &&
+            (tablePrimaries[0].alias || tablePrimaries[0].subQueryAlias)
+          ) {
+            convertTablePrimariesToSuggestions(tablePrimaries);
+          }
+          parser.expandIdentifierChain({
+            wrapper: parser.yy.result.suggestColumns,
+            anyOwner: false,
+            isColumnWrapper: true
+          });
+        }
+      } else {
+        parser.expandIdentifierChain({
+          wrapper: parser.yy.result.suggestColumns,
+          anyOwner: false,
+          isColumnWrapper: true
+        });
+      }
+    }
+
+    if (typeof parser.yy.result.colRef !== 'undefined' && !parser.yy.result.colRef.linked) {
+      parser.expandIdentifierChain({ wrapper: parser.yy.result.colRef });
+
+      const primaries = filterTablePrimariesForOwner(parser.yy.latestTablePrimaries);
+      if (
+        primaries.length === 0 ||
+        (primaries.length > 1 && parser.yy.result.colRef.identifierChain.length === 1)
+      ) {
+        parser.yy.result.colRef.identifierChain = [];
+      }
+    }
+    if (
+      typeof parser.yy.result.suggestKeyValues !== 'undefined' &&
+      !parser.yy.result.suggestKeyValues.linked
+    ) {
+      parser.expandIdentifierChain({ wrapper: parser.yy.result.suggestKeyValues });
+    }
+  };
+
+  parser.getSubQuery = function(cols) {
+    const columns = [];
+    cols.selectList.forEach(col => {
+      const result = {};
+      if (col.alias) {
+        result.alias = col.alias;
+      }
+      if (col.valueExpression && col.valueExpression.columnReference) {
+        result.identifierChain = col.valueExpression.columnReference;
+      } else if (col.asterisk) {
+        result.identifierChain = [{ asterisk: true }];
+      }
+      if (
+        col.valueExpression &&
+        col.valueExpression.types &&
+        col.valueExpression.types.length === 1
+      ) {
+        result.type = col.valueExpression.types[0];
+      }
+
+      columns.push(result);
+    });
+
+    return {
+      columns: columns
+    };
+  };
+
+  parser.addTablePrimary = function(ref) {
+    if (typeof parser.yy.latestTablePrimaries === 'undefined') {
+      parser.yy.latestTablePrimaries = [];
+    }
+    parser.yy.latestTablePrimaries.push(ref);
+  };
+
+  parser.suggestFileFormats = function() {
+    parser.suggestKeywords([
+      'AVRO',
+      'KUDU',
+      'ORC',
+      'PARQUET',
+      'RCFILE',
+      'SEQUENCEFILE',
+      'TEXTFILE'
+    ]);
+  };
+
+  parser.getKeywordsForOptionalsLR = function(optionals, keywords, override) {
+    let result = [];
+
+    for (let i = 0; i < optionals.length; i++) {
+      if (!optionals[i] && (typeof override === 'undefined' || override[i])) {
+        if (keywords[i] instanceof Array) {
+          result = result.concat(keywords[i]);
+        } else {
+          result.push(keywords[i]);
+        }
+      } else if (optionals[i]) {
+        break;
+      }
+    }
+    return result;
+  };
+
+  parser.suggestDdlAndDmlKeywords = function(extraKeywords) {
+    let keywords = [
+      'ALTER',
+      'CREATE',
+      'DESCRIBE',
+      'DROP',
+      'GRANT',
+      'INSERT',
+      'REVOKE',
+      'SELECT',
+      'SET',
+      'SHOW',
+      'TRUNCATE',
+      'UPDATE',
+      'USE',
+      'WITH'
+    ];
+
+    if (extraKeywords) {
+      keywords = keywords.concat(extraKeywords);
+    }
+
+    parser.suggestKeywords(keywords);
+  };
+
+  parser.checkForSelectListKeywords = function(selectList) {
+    if (selectList.length === 0) {
+      return;
+    }
+    const last = selectList[selectList.length - 1];
+    if (!last || !last.valueExpression) {
+      return;
+    }
+    const valueExpressionKeywords = parser.getValueExpressionKeywords(last.valueExpression);
+    let keywords = [];
+    if (last.suggestKeywords) {
+      keywords = keywords.concat(last.suggestKeywords);
+    }
+    if (valueExpressionKeywords.suggestKeywords) {
+      keywords = keywords.concat(valueExpressionKeywords.suggestKeywords);
+    }
+    if (valueExpressionKeywords.suggestColRefKeywords) {
+      parser.suggestColRefKeywords(valueExpressionKeywords.suggestColRefKeywords);
+      parser.addColRefIfExists(last.valueExpression);
+    }
+    if (!last.alias) {
+      keywords.push('AS');
+    }
+    if (keywords.length > 0) {
+      parser.suggestKeywords(keywords);
+    }
+  };
+
+  parser.checkForKeywords = function(expression) {
+    if (expression) {
+      if (expression.suggestKeywords && expression.suggestKeywords.length > 0) {
+        parser.suggestKeywords(expression.suggestKeywords);
+      }
+      if (expression.suggestColRefKeywords) {
+        parser.suggestColRefKeywords(expression.suggestColRefKeywords);
+        parser.addColRefIfExists(expression);
+      }
+    }
+  };
+
+  parser.createWeightedKeywords = function(keywords, weight) {
+    const result = [];
+    keywords.forEach(keyword => {
+      if (typeof keyword.weight !== 'undefined') {
+        keyword.weight = weight + keyword.weight / 10;
+        result.push(keyword);
+      } else {
+        result.push({ value: keyword, weight: weight });
+      }
+    });
+    return result;
+  };
+
+  parser.suggestKeywords = function(keywords) {
+    const weightedKeywords = [];
+    if (keywords.length === 0) {
+      return;
+    }
+    keywords.forEach(keyword => {
+      if (typeof keyword.weight !== 'undefined') {
+        weightedKeywords.push(keyword);
+      } else {
+        weightedKeywords.push({ value: keyword, weight: -1 });
+      }
+    });
+    weightedKeywords.sort((a, b) => {
+      if (a.weight !== b.weight) {
+        return b.weight - a.weight;
+      }
+      return a.value.localeCompare(b.value);
+    });
+    parser.yy.result.suggestKeywords = weightedKeywords;
+  };
+
+  parser.suggestColRefKeywords = function(colRefKeywords) {
+    parser.yy.result.suggestColRefKeywords = colRefKeywords;
+  };
+
+  parser.suggestTablesOrColumns = function(identifier) {
+    if (typeof parser.yy.latestTablePrimaries == 'undefined') {
+      parser.suggestTables({ identifierChain: [{ name: identifier }] });
+      return;
+    }
+    const tableRef = parser.yy.latestTablePrimaries.filter(tablePrimary => {
+      return equalIgnoreCase(tablePrimary.alias, identifier);
+    });
+    if (tableRef.length > 0) {
+      parser.suggestColumns({ identifierChain: [{ name: identifier }] });
+    } else {
+      parser.suggestTables({ identifierChain: [{ name: identifier }] });
+    }
+  };
+
+  parser.suggestFunctions = function(details) {
+    parser.yy.result.suggestFunctions = details || {};
+  };
+
+  parser.suggestAggregateFunctions = function() {
+    const primaries = [];
+    const aliases = {};
+    parser.yy.latestTablePrimaries.forEach(primary => {
+      if (typeof primary.alias !== 'undefined') {
+        aliases[primary.alias] = true;
+      }
+      // Drop if the first one refers to a table alias (...FROM tbl t, t.map tm ...)
+      if (
+        typeof primary.identifierChain !== 'undefined' &&
+        !aliases[primary.identifierChain[0].name] &&
+        typeof primary.owner === 'undefined'
+      ) {
+        primaries.push(primary);
+      }
+    });
+    parser.yy.result.suggestAggregateFunctions = { tablePrimaries: primaries };
+  };
+
+  parser.suggestAnalyticFunctions = function() {
+    parser.yy.result.suggestAnalyticFunctions = true;
+  };
+
+  parser.suggestSetOptions = function() {
+    parser.yy.result.suggestSetOptions = true;
+  };
+
+  parser.suggestIdentifiers = function(identifiers) {
+    parser.yy.result.suggestIdentifiers = identifiers;
+  };
+
+  parser.suggestColumns = function(details) {
+    if (typeof details === 'undefined') {
+      details = { identifierChain: [] };
+    } else if (typeof details.identifierChain === 'undefined') {
+      details.identifierChain = [];
+    }
+    parser.yy.result.suggestColumns = details;
+  };
+
+  parser.suggestGroupBys = function(details) {
+    parser.yy.result.suggestGroupBys = details || {};
+  };
+
+  parser.suggestOrderBys = function(details) {
+    parser.yy.result.suggestOrderBys = details || {};
+  };
+
+  parser.suggestFilters = function(details) {
+    parser.yy.result.suggestFilters = details || {};
+  };
+
+  parser.suggestKeyValues = function(details) {
+    parser.yy.result.suggestKeyValues = details || {};
+  };
+
+  parser.suggestTables = function(details) {
+    parser.yy.result.suggestTables = details || {};
+  };
+
+  const adjustLocationForCursor = function(location) {
+    // columns are 0-based and lines not, so add 1 to cols
+    const newLocation = {
+      first_line: location.first_line,
+      last_line: location.last_line,
+      first_column: location.first_column + 1,
+      last_column: location.last_column + 1
+    };
+    if (parser.yy.cursorFound) {
+      if (
+        parser.yy.cursorFound.first_line === newLocation.first_line &&
+        parser.yy.cursorFound.last_column <= newLocation.first_column
+      ) {
+        let additionalSpace = parser.yy.partialLengths.left + parser.yy.partialLengths.right;
+        additionalSpace -= parser.yy.partialCursor ? 1 : 3; // For some reason the normal cursor eats 3 positions.
+        newLocation.first_column = newLocation.first_column + additionalSpace;
+        newLocation.last_column = newLocation.last_column + additionalSpace;
+      }
+    }
+    return newLocation;
+  };
+
+  parser.addFunctionLocation = function(location, functionName) {
+    // Remove trailing '(' from location
+    const adjustedLocation = {
+      first_line: location.first_line,
+      last_line: location.last_line,
+      first_column: location.first_column,
+      last_column: location.last_column - 1
+    };
+    parser.yy.locations.push({
+      type: 'function',
+      location: adjustLocationForCursor(adjustedLocation),
+      function: functionName.toLowerCase()
+    });
+  };
+
+  parser.addStatementLocation = function(location) {
+    // Don't report lonely cursor as a statement
+    if (
+      location.first_line === location.last_line &&
+      Math.abs(location.last_column - location.first_column) === 1
+    ) {
+      return;
+    }
+    let adjustedLocation;
+    if (
+      parser.yy.cursorFound &&
+      parser.yy.cursorFound.last_line === location.last_line &&
+      parser.yy.cursorFound.first_column >= location.first_column &&
+      parser.yy.cursorFound.last_column <= location.last_column
+    ) {
+      const additionalSpace = parser.yy.partialLengths.left + parser.yy.partialLengths.right;
+      adjustedLocation = {
+        first_line: location.first_line,
+        last_line: location.last_line,
+        first_column: location.first_column + 1,
+        last_column: location.last_column + additionalSpace - (parser.yy.partialCursor ? 0 : 2)
+      };
+    } else {
+      adjustedLocation = {
+        first_line: location.first_line,
+        last_line: location.last_line,
+        first_column: location.first_column + 1,
+        last_column: location.last_column + 1
+      };
+    }
+
+    parser.yy.locations.push({
+      type: 'statement',
+      location: adjustedLocation
+    });
+  };
+
+  parser.firstDefined = function() {
+    for (let i = 0; i + 1 < arguments.length; i += 2) {
+      if (arguments[i]) {
+        return arguments[i + 1];
+      }
+    }
+  };
+
+  parser.addClauseLocation = function(type, precedingLocation, locationIfPresent, isCursor) {
+    let location;
+    if (isCursor) {
+      if (parser.yy.partialLengths.left === 0 && parser.yy.partialLengths.right === 0) {
+        location = {
+          type: type,
+          missing: true,
+          location: adjustLocationForCursor({
+            first_line: precedingLocation.last_line,
+            first_column: precedingLocation.last_column,
+            last_line: precedingLocation.last_line,
+            last_column: precedingLocation.last_column
+          })
+        };
+      } else {
+        location = {
+          type: type,
+          missing: false,
+          location: {
+            first_line: locationIfPresent.last_line,
+            first_column: locationIfPresent.last_column - 1,
+            last_line: locationIfPresent.last_line,
+            last_column:
+              locationIfPresent.last_column -
+              1 +
+              parser.yy.partialLengths.right +
+              parser.yy.partialLengths.left
+          }
+        };
+      }
+    } else {
+      location = {
+        type: type,
+        missing: !locationIfPresent,
+        location: adjustLocationForCursor(
+          locationIfPresent || {
+            first_line: precedingLocation.last_line,
+            first_column: precedingLocation.last_column,
+            last_line: precedingLocation.last_line,
+            last_column: precedingLocation.last_column
+          }
+        )
+      };
+    }
+    if (parser.isInSubquery()) {
+      location.subquery = true;
+    }
+    parser.yy.locations.push(location);
+  };
+
+  parser.addStatementTypeLocation = function(identifier, location, additionalText) {
+    // Don't add if already there except for SELECT
+    if (identifier !== 'SELECT' && parser.yy.allLocations) {
+      for (let i = parser.yy.allLocations.length - 1; i >= 0; i--) {
+        if (parser.yy.allLocations[i] && parser.yy.allLocations[i].type === 'statement') {
+          break;
+        }
+        if (parser.yy.allLocations[i] && parser.yy.allLocations[i].type === 'statementType') {
+          return;
+        }
+      }
+    }
+    const loc = {
+      type: 'statementType',
+      location: adjustLocationForCursor(location),
+      identifier: identifier
+    };
+    if (typeof additionalText !== 'undefined') {
+      switch (identifier) {
+        case 'ALTER':
+          if (/ALTER\s+VIEW/i.test(additionalText)) {
+            loc.identifier = 'ALTER VIEW';
+          } else {
+            loc.identifier = 'ALTER TABLE';
+          }
+          break;
+        case 'COMPUTE':
+          loc.identifier = 'COMPUTE STATS';
+          break;
+        case 'CREATE':
+          if (/CREATE\s+VIEW/i.test(additionalText)) {
+            loc.identifier = 'CREATE VIEW';
+          } else if (/CREATE\s+TABLE/i.test(additionalText)) {
+            loc.identifier = 'CREATE TABLE';
+          } else if (/CREATE\s+DATABASE/i.test(additionalText)) {
+            loc.identifier = 'CREATE DATABASE';
+          } else if (/CREATE\s+ROLE/i.test(additionalText)) {
+            loc.identifier = 'CREATE ROLE';
+          } else if (/CREATE\s+FUNCTION/i.test(additionalText)) {
+            loc.identifier = 'CREATE FUNCTION';
+          } else {
+            loc.identifier = 'CREATE TABLE';
+          }
+          break;
+        case 'DROP':
+          if (/DROP\s+VIEW/i.test(additionalText)) {
+            loc.identifier = 'DROP VIEW';
+          } else if (/DROP\s+TABLE/i.test(additionalText)) {
+            loc.identifier = 'DROP TABLE';
+          } else if (/DROP\s+DATABASE/i.test(additionalText)) {
+            loc.identifier = 'DROP DATABASE';
+          } else if (/DROP\s+ROLE/i.test(additionalText)) {
+            loc.identifier = 'DROP ROLE';
+          } else if (/DROP\s+STATS/i.test(additionalText)) {
+            loc.identifier = 'DROP STATS';
+          } else if (/DROP\s+FUNCTION/i.test(additionalText)) {
+            loc.identifier = 'DROP FUNCTION';
+          } else {
+            loc.identifier = 'DROP TABLE';
+          }
+          break;
+        case 'INVALIDATE':
+          loc.identifier = 'INVALIDATE METADATA';
+          break;
+        case 'LOAD':
+          loc.identifier = 'LOAD DATA';
+          break;
+        case 'TRUNCATE':
+          loc.identifier = 'TRUNCATE TABLE';
+          break;
+        default:
+      }
+    }
+    parser.yy.locations.push(loc);
+  };
+
+  parser.addFileLocation = function(location, path) {
+    parser.yy.locations.push({
+      type: 'file',
+      location: adjustLocationForCursor(location),
+      path: path
+    });
+  };
+
+  parser.addDatabaseLocation = function(location, identifierChain) {
+    parser.yy.locations.push({
+      type: 'database',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addTableLocation = function(location, identifierChain) {
+    parser.yy.locations.push({
+      type: 'table',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addColumnAliasLocation = function(location, alias, parentLocation) {
+    const aliasLocation = {
+      type: 'alias',
+      source: 'column',
+      alias: alias,
+      location: adjustLocationForCursor(location),
+      parentLocation: adjustLocationForCursor(parentLocation)
+    };
+    if (
+      parser.yy.locations.length &&
+      parser.yy.locations[parser.yy.locations.length - 1].type === 'column'
+    ) {
+      const closestColumn = parser.yy.locations[parser.yy.locations.length - 1];
+      if (
+        closestColumn.location.first_line === aliasLocation.parentLocation.first_line &&
+        closestColumn.location.last_line === aliasLocation.parentLocation.last_line &&
+        closestColumn.location.first_column === aliasLocation.parentLocation.first_column &&
+        closestColumn.location.last_column === aliasLocation.parentLocation.last_column
+      ) {
+        parser.yy.locations[parser.yy.locations.length - 1].alias = alias;
+      }
+    }
+    parser.yy.locations.push(aliasLocation);
+  };
+
+  parser.addTableAliasLocation = function(location, alias, identifierChain) {
+    parser.yy.locations.push({
+      type: 'alias',
+      source: 'table',
+      alias: alias,
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addSubqueryAliasLocation = function(location, alias) {
+    parser.yy.locations.push({
+      type: 'alias',
+      source: 'subquery',
+      alias: alias,
+      location: adjustLocationForCursor(location)
+    });
+  };
+
+  parser.addAsteriskLocation = function(location, identifierChain) {
+    parser.yy.locations.push({
+      type: 'asterisk',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addVariableLocation = function(location, value) {
+    if (/\${[^}]*}/.test(value)) {
+      parser.yy.locations.push({
+        type: 'variable',
+        location: adjustLocationForCursor(location),
+        value: value
+      });
+    }
+  };
+
+  parser.addColumnLocation = function(location, identifierChain) {
+    const isVariable =
+      identifierChain.length && /\${[^}]*}/.test(identifierChain[identifierChain.length - 1].name);
+    if (isVariable) {
+      parser.yy.locations.push({
+        type: 'variable',
+        location: adjustLocationForCursor(location),
+        value: identifierChain[identifierChain.length - 1].name
+      });
+    } else {
+      parser.yy.locations.push({
+        type: 'column',
+        location: adjustLocationForCursor(location),
+        identifierChain: identifierChain,
+        qualified: identifierChain.length > 1
+      });
+    }
+  };
+
+  parser.addCteAliasLocation = function(location, alias) {
+    parser.yy.locations.push({
+      type: 'alias',
+      source: 'cte',
+      alias: alias,
+      location: adjustLocationForCursor(location)
+    });
+  };
+
+  parser.addUnknownLocation = function(location, identifierChain) {
+    const isVariable =
+      identifierChain.length && /\${[^}]*}/.test(identifierChain[identifierChain.length - 1].name);
+    let loc;
+    if (isVariable) {
+      loc = {
+        type: 'variable',
+        location: adjustLocationForCursor(location),
+        value: identifierChain[identifierChain.length - 1].name
+      };
+    } else {
+      loc = {
+        type: 'unknown',
+        location: adjustLocationForCursor(location),
+        identifierChain: identifierChain,
+        qualified: identifierChain.length > 1
+      };
+    }
+    parser.yy.locations.push(loc);
+    return loc;
+  };
+
+  parser.addNewDatabaseLocation = function(location, identifierChain) {
+    parser.yy.definitions.push({
+      type: 'database',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addNewTableLocation = function(location, identifierChain, colSpec) {
+    const columns = [];
+    if (colSpec) {
+      colSpec.forEach(col => {
+        columns.push({
+          identifierChain: [col.identifier], // TODO: Complex
+          type: col.type,
+          location: adjustLocationForCursor(col.location)
+        });
+      });
+    }
+    parser.yy.definitions.push({
+      type: 'table',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain,
+      columns: columns
+    });
+  };
+
+  parser.addColRefToVariableIfExists = function(left, right) {
+    if (
+      left &&
+      left.columnReference &&
+      left.columnReference.length &&
+      right &&
+      right.columnReference &&
+      right.columnReference.length &&
+      parser.yy.locations.length > 1
+    ) {
+      const addColRefToVariableLocation = function(variableValue, colRef) {
+        // See if colref is actually an alias
+        if (colRef.length === 1 && colRef[0].name) {
+          parser.yy.locations.some(location => {
+            if (location.type === 'column' && location.alias === colRef[0].name) {
+              colRef = location.identifierChain;
+              return true;
+            }
+          });
+        }
+
+        for (let i = parser.yy.locations.length - 1; i > 0; i--) {
+          const location = parser.yy.locations[i];
+          if (location.type === 'variable' && location.value === variableValue) {
+            location.colRef = { identifierChain: colRef };
+            break;
+          }
+        }
+      };
+
+      if (/\${[^}]*}/.test(left.columnReference[0].name)) {
+        // left is variable
+        addColRefToVariableLocation(left.columnReference[0].name, right.columnReference);
+      } else if (/\${[^}]*}/.test(right.columnReference[0].name)) {
+        // right is variable
+        addColRefToVariableLocation(right.columnReference[0].name, left.columnReference);
+      }
+    }
+  };
+
+  parser.suggestDatabases = function(details) {
+    parser.yy.result.suggestDatabases = details || {};
+  };
+
+  parser.suggestHdfs = function(details) {
+    parser.yy.result.suggestHdfs = details || {};
+  };
+
+  parser.suggestValues = function(details) {
+    parser.yy.result.suggestValues = details || {};
+  };
+
+  parser.determineCase = function(text) {
+    if (!parser.yy.caseDetermined) {
+      parser.yy.lowerCase = text.toLowerCase() === text;
+      parser.yy.caseDetermined = true;
+    }
+  };
+
+  parser.handleQuotedValueWithCursor = function(lexer, yytext, yylloc, quoteChar) {
+    if (yytext.indexOf('\u2020') !== -1 || yytext.indexOf('\u2021') !== -1) {
+      parser.yy.partialCursor = yytext.indexOf('\u2021') !== -1;
+      const cursorIndex = parser.yy.partialCursor
+        ? yytext.indexOf('\u2021')
+        : yytext.indexOf('\u2020');
+      parser.yy.cursorFound = {
+        first_line: yylloc.first_line,
+        last_line: yylloc.last_line,
+        first_column: yylloc.first_column + cursorIndex,
+        last_column: yylloc.first_column + cursorIndex + 1
+      };
+      const remainder = yytext.substring(cursorIndex + 1);
+      const remainingQuotes = (lexer.upcomingInput().match(new RegExp(quoteChar, 'g')) || [])
+        .length;
+      if (remainingQuotes > 0 && (remainingQuotes & 1) !== 0) {
+        parser.yy.missingEndQuote = false;
+        lexer.input();
+      } else {
+        parser.yy.missingEndQuote = true;
+        lexer.unput(remainder);
+      }
+      lexer.popState();
+      return true;
+    }
+    return false;
+  };
+
+  let lexerModified = false;
+
+  /**
+   * Main parser function
+   */
+  parser.parseSql = function(beforeCursor, afterCursor, debug) {
+    // Jison counts CRLF as two lines in the locations
+    beforeCursor = beforeCursor.replace(/\r\n|\n\r/gm, '\n');
+    afterCursor = afterCursor.replace(/\r\n|\n\r/gm, '\n');
+    parser.yy.result = { locations: [] };
+    parser.yy.lowerCase = false;
+    parser.yy.locations = [];
+    parser.yy.definitions = [];
+    parser.yy.allLocations = [];
+    parser.yy.subQueries = [];
+    parser.yy.errors = [];
+    parser.yy.selectListAliases = [];
+    parser.yy.activeDialect = 'generic';
+
+    parser.yy.locationsStack = [];
+    parser.yy.primariesStack = [];
+    parser.yy.subQueriesStack = [];
+    parser.yy.resultStack = [];
+    parser.yy.selectListAliasesStack = [];
+
+    delete parser.yy.caseDetermined;
+    delete parser.yy.cursorFound;
+    delete parser.yy.partialCursor;
+
+    // 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
+      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;
+  };
+};
+
+const SYNTAX_PARSER_NOOP_FUNCTIONS = [
+  'addAsteriskLocation',
+  'addClauseLocation',
+  'addColRefIfExists',
+  'addColRefToVariableIfExists',
+  'addColumnAliasLocation',
+  'addColumnLocation',
+  'addCommonTableExpressions',
+  'addCteAliasLocation',
+  'addDatabaseLocation',
+  'addFileLocation',
+  'addFunctionLocation',
+  'addNewDatabaseLocation',
+  'addNewTableLocation',
+  'addStatementLocation',
+  'addStatementTypeLocation',
+  'addSubqueryAliasLocation',
+  'addTableAliasLocation',
+  'addTableLocation',
+  'addTablePrimary',
+  'addUnknownLocation',
+  'addVariableLocation',
+  'applyArgumentTypesToSuggestions',
+  'applyTypeToSuggestions',
+  'checkForKeywords',
+  'checkForSelectListKeywords',
+  'commitLocations',
+  'firstDefined',
+  'getSelectListKeywords',
+  'getSubQuery',
+  'getValueExpressionKeywords',
+  'identifyPartials',
+  'popQueryState',
+  'prepareNewStatement',
+  'pushQueryState',
+  'selectListNoTableSuggest',
+  'suggestAggregateFunctions',
+  'suggestAnalyticFunctions',
+  'suggestColRefKeywords',
+  'suggestColumns',
+  'suggestDatabases',
+  'suggestDdlAndDmlKeywords',
+  'suggestFileFormats',
+  'suggestFilters',
+  'suggestFunctions',
+  'suggestGroupBys',
+  'suggestHdfs',
+  'suggestIdentifiers',
+  'suggestJoinConditions',
+  'suggestJoins',
+  'suggestKeyValues',
+  'suggestKeywords',
+  'suggestOrderBys',
+  'suggestSelectListAliases',
+  'suggestTables',
+  'suggestTablesOrColumns',
+  'suggestValueExpressionKeywords',
+  'suggestValues',
+  'valueExpressionSuggest'
+];
+
+const SYNTAX_PARSER_NOOP = function() {};
+
+const initSyntaxParser = function(parser) {
+  // Noop functions for compatibility with the autocomplete parser as the grammar is shared
+  SYNTAX_PARSER_NOOP_FUNCTIONS.forEach(noopFn => {
+    parser[noopFn] = SYNTAX_PARSER_NOOP;
+  });
+
+  parser.yy.locations = [{}];
+
+  parser.determineCase = function(text) {
+    if (!parser.yy.caseDetermined) {
+      parser.yy.lowerCase = text.toLowerCase() === text;
+      parser.yy.caseDetermined = true;
+    }
+  };
+
+  parser.getKeywordsForOptionalsLR = function() {
+    return [];
+  };
+
+  parser.mergeSuggestKeywords = function() {
+    return {};
+  };
+
+  parser.getTypeKeywords = function() {
+    return [];
+  };
+
+  parser.getColumnDataTypeKeywords = function() {
+    return [];
+  };
+
+  parser.findCaseType = function() {
+    return { types: ['T'] };
+  };
+
+  parser.findReturnTypes = function() {
+    return ['T'];
+  };
+
+  parser.expandIdentifierChain = function() {
+    return [];
+  };
+
+  parser.createWeightedKeywords = function() {
+    return [];
+  };
+
+  parser.handleQuotedValueWithCursor = function(lexer, yytext, yylloc, quoteChar) {
+    if (yytext.indexOf('\u2020') !== -1 || yytext.indexOf('\u2021') !== -1) {
+      parser.yy.partialCursor = yytext.indexOf('\u2021') !== -1;
+      const cursorIndex = parser.yy.partialCursor
+        ? yytext.indexOf('\u2021')
+        : yytext.indexOf('\u2020');
+      parser.yy.cursorFound = {
+        first_line: yylloc.first_line,
+        last_line: yylloc.last_line,
+        first_column: yylloc.first_column + cursorIndex,
+        last_column: yylloc.first_column + cursorIndex + 1
+      };
+      const remainder = yytext.substring(cursorIndex + 1);
+      const remainingQuotes = (lexer.upcomingInput().match(new RegExp(quoteChar, 'g')) || [])
+        .length;
+      if (remainingQuotes > 0 && (remainingQuotes & 1) !== 0) {
+        parser.yy.missingEndQuote = false;
+        lexer.input();
+      } else {
+        parser.yy.missingEndQuote = true;
+        lexer.unput(remainder);
+      }
+      lexer.popState();
+      return true;
+    }
+    return false;
+  };
+
+  parser.yy.parseError = function(str, hash) {
+    parser.yy.error = hash;
+  };
+
+  const IGNORED_EXPECTED = {
+    ';': true,
+    '.': true,
+    EOF: true,
+    UNSIGNED_INTEGER: true,
+    UNSIGNED_INTEGER_E: true,
+    REGULAR_IDENTIFIER: true,
+    CURSOR: true,
+    PARTIAL_CURSOR: true,
+    HDFS_START_QUOTE: true,
+    HDFS_PATH: true,
+    HDFS_END_QUOTE: true,
+    COMPARISON_OPERATOR: true, // TODO: Expand in results when found
+    ARITHMETIC_OPERATOR: true, // TODO: Expand in results when found
+    VARIABLE_REFERENCE: true,
+    BACKTICK: true,
+    VALUE: true,
+    PARTIAL_VALUE: true,
+    SINGLE_QUOTE: true,
+    DOUBLE_QUOTE: true
+  };
+
+  const CLEAN_EXPECTED = {
+    BETWEEN_AND: 'AND',
+    OVERWRITE_DIRECTORY: 'OVERWRITE',
+    STORED_AS_DIRECTORIES: 'STORED',
+    LIKE_PARQUET: 'LIKE',
+    PARTITION_VALUE: 'PARTITION'
+  };
+
+  parser.parseSyntax = function(beforeCursor, afterCursor, debug) {
+    parser.yy.caseDetermined = false;
+    parser.yy.error = undefined;
+
+    parser.yy.latestTablePrimaries = [];
+    parser.yy.subQueries = [];
+    parser.yy.selectListAliases = [];
+    parser.yy.latestTablePrimaries = [];
+
+    parser.yy.activeDialect = 'generic';
+
+    // TODO: Find a way around throwing an exception when the parser finds a syntax error
+    try {
+      parser.yy.error = false;
+      parser.parse(beforeCursor + afterCursor);
+    } catch (err) {
+      if (debug) {
+        console.warn(err);
+        console.warn(err.stack);
+        console.warn(parser.yy.error);
+      }
+    }
+
+    if (
+      parser.yy.error &&
+      (parser.yy.error.loc.last_column < beforeCursor.length ||
+        !beforeCursor.endsWith(parser.yy.error.text))
+    ) {
+      const weightedExpected = [];
+
+      const addedExpected = {};
+
+      const isLowerCase =
+        (parser.yy.caseDetermined && parser.yy.lowerCase) ||
+        parser.yy.error.text.toLowerCase() === parser.yy.error.text;
+
+      if (
+        parser.yy.error.expected.length === 2 &&
+        parser.yy.error.expected.indexOf("';'") !== -1 &&
+        parser.yy.error.expected.indexOf("'EOF'") !== -1
+      ) {
+        parser.yy.error.expected = [];
+        parser.yy.error.expectedStatementEnd = true;
+        return parser.yy.error;
+      }
+      for (let i = 0; i < parser.yy.error.expected.length; i++) {
+        let expected = parser.yy.error.expected[i];
+        // Strip away the surrounding ' chars
+        expected = expected.substring(1, expected.length - 1);
+        // TODO: Only suggest alphanumeric?
+        if (expected === 'REGULAR_IDENTIFIER') {
+          parser.yy.error.expectedIdentifier = true;
+          if (/^<[a-z]+>/.test(parser.yy.error.token)) {
+            const text = '`' + parser.yy.error.text + '`';
+            weightedExpected.push({
+              text: text,
+              distance: stringDistance(parser.yy.error.text, text, true)
+            });
+            parser.yy.error.possibleReserved = true;
+          }
+        } else if (!IGNORED_EXPECTED[expected] && /[a-z_]+/i.test(expected)) {
+          if (/^<[a-z]+>/.test(expected)) {
+            continue;
+          }
+          expected = CLEAN_EXPECTED[expected] || expected;
+          if (expected === parser.yy.error.text.toUpperCase()) {
+            // Can happen when the lexer entry for a rule contains multiple words like 'stored' in 'stored as parquet'
+            return false;
+          }
+          const text = isLowerCase ? expected.toLowerCase() : expected;
+          if (text && !addedExpected[text]) {
+            addedExpected[text] = true;
+            weightedExpected.push({
+              text: text,
+              distance: stringDistance(parser.yy.error.text, text, true)
+            });
+          }
+        }
+      }
+      if (weightedExpected.length === 0) {
+        parser.yy.error.expected = [];
+        parser.yy.error.incompleteStatement = true;
+        return parser.yy.error;
+      }
+      weightedExpected.sort((a, b) => {
+        if (a.distance === b.distance) {
+          return a.text.localeCompare(b.text);
+        }
+        return a.distance - b.distance;
+      });
+      parser.yy.error.expected = weightedExpected;
+      parser.yy.error.incompleteStatement = true;
+      return parser.yy.error;
+    } else if (parser.yy.error) {
+      parser.yy.error.expected = [];
+      parser.yy.error.incompleteStatement = true;
+      return parser.yy.error;
+    }
+    return false;
+  };
+};
+
+export default {
+  initSqlParser: initSqlParser,
+  initSyntaxParser: initSyntaxParser
+};

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

@@ -21,12 +21,14 @@
 const AUTOCOMPLETE_MODULES = {
   generic: () => import(/* webpackChunkName: "generic-parser" */ 'parse/sql/generic/genericAutocompleteParser'),
   hive: () => import(/* webpackChunkName: "hive-parser" */ 'parse/sql/hive/hiveAutocompleteParser'),
-  impala: () => import(/* webpackChunkName: "impala-parser" */ 'parse/sql/impala/impalaAutocompleteParser')
+  impala: () => import(/* webpackChunkName: "impala-parser" */ 'parse/sql/impala/impalaAutocompleteParser'),
+  ksql: () => import(/* webpackChunkName: "ksql-parser" */ 'parse/sql/ksql/ksqlAutocompleteParser')
 };
 const SYNTAX_MODULES = {
   generic: () => import(/* webpackChunkName: "generic-parser" */ 'parse/sql/generic/genericSyntaxParser'),
   hive: () => import(/* webpackChunkName: "hive-parser" */ 'parse/sql/hive/hiveSyntaxParser'),
-  impala: () => import(/* webpackChunkName: "impala-parser" */ 'parse/sql/impala/impalaSyntaxParser')
+  impala: () => import(/* webpackChunkName: "impala-parser" */ 'parse/sql/impala/impalaSyntaxParser'),
+  ksql: () => import(/* webpackChunkName: "ksql-parser" */ 'parse/sql/ksql/ksqlSyntaxParser')
 };
 /* eslint-enable */
 

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