Преглед изворни кода

[Phoenix Parser] adding autocomplete support of 'UPSERT' and removing 'INSERT'

ayush.goyal пре 4 година
родитељ
комит
c7cac77036

+ 226 - 0
desktop/core/src/desktop/js/parse/jison/sql/phoenix/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'; }
+'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'; }
+'UPSERT'                                   { parser.determineCase(yytext); return 'UPSERT'; }
+'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>.                       { }

+ 3 - 3
desktop/core/src/desktop/js/parse/jison/sql/phoenix/structure.json

@@ -1,5 +1,5 @@
 {
-  "lexer": "../generic/sql.jisonlex",
+  "lexer": "sql.jisonlex",
   "autocomplete": [
     "../generic/autocomplete_header.jison",
     "../generic/alter/alter_common.jison",
@@ -15,7 +15,6 @@
     "../generic/drop/drop_role.jison",
     "../generic/drop/drop_table.jison",
     "../generic/drop/drop_view.jison",
-    "../generic/insert/insert.jison",
     "../generic/select/cte_select_statement.jison",
     "../generic/select/from_clause.jison",
     "../generic/select/group_by_clause.jison",
@@ -53,6 +52,7 @@
     "../generic/use/use.jison",
     "../generic/sql_error.jison",
     "../generic/sql_main.jison",
+    "upsert/upsert.jison",
     "../calcite/quoted_table_identifier.jison",
     "../generic/sql_valueExpression.jison",
     "../generic/autocomplete_footer.jison"
@@ -72,7 +72,6 @@
     "../generic/drop/drop_role.jison",
     "../generic/drop/drop_table.jison",
     "../generic/drop/drop_view.jison",
-    "../generic/insert/insert.jison",
     "../generic/select/cte_select_statement.jison",
     "../generic/select/from_clause.jison",
     "../generic/select/group_by_clause.jison",
@@ -109,6 +108,7 @@
     "../generic/update/update_table.jison",
     "../generic/use/use.jison",
     "../generic/sql_main.jison",
+    "upsert/upsert.jison",
     "../calcite/quoted_table_identifier.jison",
     "../generic/sql_valueExpression.jison",
     "../generic/syntax_footer.jison"

+ 88 - 0
desktop/core/src/desktop/js/parse/jison/sql/phoenix/upsert/upsert.jison

@@ -0,0 +1,88 @@
+// 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
+ : UpsertStatement
+ ;
+
+UpsertStatement
+ : UpsertValuesStatement
+ ;
+
+DataManipulation_EDIT
+ : UpsertValuesStatement_EDIT
+ ;
+
+UpsertValuesStatement
+ : 'UPSERT' 'INTO' SchemaQualifiedTableIdentifier OptionalParenthesizedColumnList 'VALUES' InsertValuesList
+   {
+     $3.owner = 'upsert';
+     parser.addTablePrimary($3);
+   }
+ | 'UPSERT' 'INTO' SchemaQualifiedTableIdentifier OptionalParenthesizedColumnList SelectStatement
+   {
+     $3.owner = 'upsert';
+     parser.addTablePrimary($3);
+   }
+ ;
+
+UpsertValuesStatement_EDIT
+ : 'UPSERT' 'CURSOR'
+   {
+     parser.suggestKeywords(['INTO']);
+   }
+ | 'UPSERT' 'INTO' 'CURSOR'
+   {
+     parser.suggestTables();
+     parser.suggestDatabases({ appendDot: true });
+   }
+ | 'UPSERT' 'INTO'  SchemaQualifiedTableIdentifier_EDIT OptionalParenthesizedColumnList SelectStatement
+ | 'UPSERT' 'INTO'  SchemaQualifiedTableIdentifier OptionalParenthesizedColumnList SelectStatement_EDIT
+ | 'UPSERT' 'INTO'  SchemaQualifiedTableIdentifier_EDIT OptionalParenthesizedColumnList OptionalValues
+ | 'UPSERT' 'INTO'  SchemaQualifiedTableIdentifier OptionalParenthesizedColumnList_EDIT OptionalValues
+   {
+     $3.owner = 'upsert';
+     parser.addTablePrimary($3);
+     if (parser.yy.result.suggestColumns) {
+       parser.yy.result.suggestColumns.owner = 'upsert';
+     }
+   }
+ | 'UPSERT' 'INTO' SchemaQualifiedTableIdentifier OptionalParenthesizedColumnList 'CURSOR' OptionalInsertValuesList
+   {
+     $3.owner = 'upsert';
+     parser.addTablePrimary($3);
+     parser.suggestKeywords(['VALUES', 'SELECT']);
+   }
+ ;
+
+OptionalValues
+ :
+ | 'VALUES' OptionalInsertValuesList
+ ;
+
+OptionalInsertValuesList
+ :
+ | InsertValuesList
+ ;
+
+InsertValuesList
+ : ParenthesizedRowValuesList
+ | InsertValuesList ',' ParenthesizedRowValuesList
+ ;
+
+ParenthesizedRowValuesList
+ : '(' InValueList ')'
+ ;

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
desktop/core/src/desktop/js/parse/sql/phoenix/phoenixAutocompleteParser.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
desktop/core/src/desktop/js/parse/sql/phoenix/phoenixSyntaxParser.js


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

@@ -1008,13 +1008,13 @@ const initSqlParser = function (parser) {
       'DESCRIBE',
       'DROP',
       'GRANT',
-      'INSERT',
       'REVOKE',
       'SELECT',
       'SET',
       'SHOW',
       'TRUNCATE',
       'UPDATE',
+      'UPSERT',
       'USE',
       'WITH'
     ];

+ 34 - 25
desktop/core/src/desktop/js/parse/sql/phoenix/test/phoenixAutocompleteParser.Insert.test.js → desktop/core/src/desktop/js/parse/sql/phoenix/test/phoenixAutocompleteParser.Upsert.test.js

@@ -16,7 +16,7 @@
 
 import phoenixAutocompleteParser from '../phoenixAutocompleteParser';
 
-describe('phoenixAutocompleteParser.js INSERT statements', () => {
+describe('phoenixAutocompleteParser.js UPSERT statements', () => {
   beforeAll(() => {
     phoenixAutocompleteParser.yy.parseError = function (msg) {
       throw Error(msg);
@@ -35,10 +35,10 @@ describe('phoenixAutocompleteParser.js INSERT statements', () => {
     ).toEqualDefinition(testDefinition);
   };
 
-  describe('INSERT', () => {
-    it('should handle "INSERT INTO bla.boo VALUES (1, 2, \'a\', 3); |"', () => {
+  describe('UPSERT', () => {
+    it('should handle "UPSERT INTO bla.boo VALUES(1, 2, 3); |"', () => {
       assertAutoComplete({
-        beforeCursor: "INSERT INTO bla.boo VALUES (1, 2, 'a', 3); ",
+        beforeCursor: 'UPSERT INTO bla.boo VALUES(1, 2, 3); ',
         afterCursor: '',
         noErrors: true,
         containsKeywords: ['SELECT'],
@@ -52,7 +52,7 @@ describe('phoenixAutocompleteParser.js INSERT statements', () => {
       assertAutoComplete({
         beforeCursor: '',
         afterCursor: '',
-        containsKeywords: ['INSERT'],
+        containsKeywords: ['UPSERT'],
         noErrors: true,
         expectedResult: {
           lowerCase: false
@@ -60,9 +60,9 @@ describe('phoenixAutocompleteParser.js INSERT statements', () => {
       });
     });
 
-    it('should suggest keywords for "INSERT |"', () => {
+    it('should suggest keywords for "UPSERT |"', () => {
       assertAutoComplete({
-        beforeCursor: 'INSERT ',
+        beforeCursor: 'UPSERT ',
         afterCursor: '',
         containsKeywords: ['INTO'],
         noErrors: true,
@@ -72,23 +72,22 @@ describe('phoenixAutocompleteParser.js INSERT statements', () => {
       });
     });
 
-    it('should suggest tables for "INSERT INTO |"', () => {
+    it('should suggest tables for "UPSERT INTO |"', () => {
       assertAutoComplete({
-        beforeCursor: 'INSERT INTO ',
+        beforeCursor: 'UPSERT INTO ',
         afterCursor: '',
         noErrors: true,
         expectedResult: {
           lowerCase: false,
           suggestTables: {},
-          suggestDatabases: { appendDot: true },
-          suggestKeywords: ['TABLE']
+          suggestDatabases: { appendDot: true }
         }
       });
     });
 
-    it('should suggest tables for "INSERT INTO baa.|"', () => {
+    it('should suggest tables for "UPSERT INTO baa.|"', () => {
       assertAutoComplete({
-        beforeCursor: 'INSERT INTO baa.',
+        beforeCursor: 'UPSERT INTO baa.',
         afterCursor: '',
         noErrors: true,
         expectedResult: {
@@ -98,36 +97,46 @@ describe('phoenixAutocompleteParser.js INSERT statements', () => {
       });
     });
 
-    it('should suggest tables for "INSERT INTO TABLE baa.|"', () => {
+    it('should suggest keywords for "UPSERT INTO baa |"', () => {
       assertAutoComplete({
-        beforeCursor: 'INSERT INTO TABLE baa.',
+        beforeCursor: 'UPSERT INTO baa ',
         afterCursor: '',
+        containsKeywords: ['VALUES', 'SELECT'],
         noErrors: true,
         expectedResult: {
-          lowerCase: false,
-          suggestTables: { identifierChain: [{ name: 'baa' }] }
+          lowerCase: false
         }
       });
     });
 
-    it('should suggest keywords for "INSERT INTO baa |"', () => {
+    it('should suggest tables for "UPSERT INTO baa SELECT * FROM |"', () => {
       assertAutoComplete({
-        beforeCursor: 'INSERT INTO baa ',
+        beforeCursor: 'UPSERT INTO baa SELECT * FROM ',
         afterCursor: '',
-        containsKeywords: ['VALUES'],
         noErrors: true,
         expectedResult: {
-          lowerCase: false
+          lowerCase: false,
+          suggestTables: {},
+          suggestDatabases: { appendDot: true }
         }
       });
     });
 
-    it('should suggest keywords for "INSERT INTO TABLE baa |"', () => {
+    it('should suggest columns for "UPSERT INTO boo.baa (|"', () => {
       assertAutoComplete({
-        beforeCursor: 'INSERT INTO TABLE baa ',
+        beforeCursor: 'UPSERT INTO boo.baa (',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestColumns: { tables: [{ identifierChain: [{ name: 'boo' }, { name: 'baa' }] }] }
+        }
+      });
+    });
+
+    it('should not suggest keywords for "UPSERT INTO boo.baa (a, b) VALUES (1, 2) |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'UPSERT INTO boo.baa (a, b) VALUES (1, 2) ',
         afterCursor: '',
-        containsKeywords: ['VALUES'],
-        noErrors: true,
         expectedResult: {
           lowerCase: false
         }

Неке датотеке нису приказане због велике количине промена