浏览代码

HUE-4419 [editor] The new autocompleter should suggest columns from sub-queries

This also includes the following fixes:

- Better error handling for invalid statements
- LIKE and RLIKE now accept value expressions
- Improved keywords completion around JOIN
Johan Ahlen 9 年之前
父节点
当前提交
008a132

文件差异内容过多而无法显示
+ 339 - 216
desktop/core/src/desktop/static/desktop/js/autocomplete/sql.jison


文件差异内容过多而无法显示
+ 0 - 0
desktop/core/src/desktop/static/desktop/js/autocomplete/sql.js


+ 92 - 52
desktop/core/src/desktop/static/desktop/js/sqlAutocompleter2.js

@@ -39,7 +39,7 @@
 
   SqlAutocompleter2.prototype.autocomplete = function (beforeCursor, afterCursor, callback, editor) {
     var self = this;
-    var parseResult = sqlParser.parseSql(beforeCursor, afterCursor, self.snippet.type(), sqlFunctions);
+    var parseResult = sqlParser.parseSql(beforeCursor, afterCursor, self.snippet.type(), sqlFunctions, true);
 
     var deferrals = [];
     var completions = [];
@@ -258,67 +258,104 @@
     return tableDeferred;
   };
 
+  SqlAutocompleter2.prototype.locateSubQuery = function (subQueries, subQueryName) {
+    var foundSubQueries = subQueries.filter(function (knownSubQuery) {
+      return knownSubQuery.alias === subQueryName
+    });
+    if (foundSubQueries.length > 0) {
+      return foundSubQueries[0];
+    }
+    return null;
+  };
+
   SqlAutocompleter2.prototype.addColumns = function (parseResult, editor, database, types, completions) {
     var self = this;
     var addColumnsDeferred = $.Deferred();
 
-    var callback = function (data) {
-      if (data.extended_columns) {
-        data.extended_columns.forEach(function (column) {
-          if (column.type.indexOf('map') === 0 && self.snippet.type() === 'hive') {
-            completions.push({value: self.backTickIfNeeded(column.name) + '[]', meta: 'map', type: 'column'})
-          } else if (column.type.indexOf('map') === 0) {
-            completions.push({value: self.backTickIfNeeded(column.name), meta: 'map', type: 'column'})
-          } else if (column.type.indexOf('struct') === 0) {
-            completions.push({value: self.backTickIfNeeded(column.name), meta: 'struct', type: 'column'})
-          } else if (column.type.indexOf('array') === 0 && self.snippet.type() === 'hive') {
-            completions.push({value: self.backTickIfNeeded(column.name) + '[]', meta: 'array', type: 'column'})
-          } else if (column.type.indexOf('array') === 0) {
-            completions.push({value: self.backTickIfNeeded(column.name), meta: 'array', type: 'column'})
-          } else if (sqlFunctions.matchesType(self.snippet.type(), types, [column.type.toUpperCase()]) ||
-              sqlFunctions.matchesType(self.snippet.type(), [column.type.toUpperCase()], types)) {
-            completions.push({value: self.backTickIfNeeded(column.name), meta: column.type, type: 'column'})
+    if (parseResult.suggestColumns.subQuery && !parseResult.suggestColumns.identifierChain) {
+      var foundSubQuery = self.locateSubQuery(parseResult.subQueries, parseResult.suggestColumns.subQuery);
+
+      var addSubQueryColumns = function (subQueryColumns) {
+        subQueryColumns.forEach(function (column) {
+          if (column.alias || column.identifierChain) {
+            // TODO: Potentially fetch column types for sub-queries, possible performance hit.
+            var type = typeof column.type !== 'undefined' && column.type !== 'COLREF' ? column.type : 'T';
+            if (column.alias) {
+              completions.push({value: self.backTickIfNeeded(column.alias), meta: type, type: 'column'})
+            } else if (column.identifierChain && column.identifierChain.length === 1) {
+              completions.push({value: self.backTickIfNeeded(column.identifierChain[0].name), meta: type, type: 'column'})
+            }
+            addColumnsDeferred.resolve();
+            return addColumnsDeferred;
+          } else if (column.subQuery && foundSubQuery.subQueries) {
+            var foundNestedSubQuery = self.locateSubQuery(foundSubQuery.subQueries, column.subQuery);
+            if (foundNestedSubQuery !== null) {
+              addSubQueryColumns(foundNestedSubQuery.columns);
+            }
           }
         });
-      } else if (data.columns) {
-        data.columns.forEach(function (column) {
-          completions.push({value: self.backTickIfNeeded(column), meta: 'column', type: 'column'})
-        });
-      }
-      if (data.type === 'map' && self.snippet.type() === 'impala') {
-        completions.push({value: 'key', meta: 'key', type: 'column'});
-        completions.push({value: 'value', meta: 'value', type: 'column'});
+      };
+      if (foundSubQuery !== null) {
+        addSubQueryColumns(foundSubQuery.columns);
       }
-      if (data.type === 'struct') {
-        data.fields.forEach(function (field) {
-          completions.push({value: self.backTickIfNeeded(field.name), meta: 'struct', type: 'column'})
-        });
-      } else if (data.type === 'map' && (data.value && data.value.fields)) {
-        data.value.fields.forEach(function (field) {
-          if (sqlFunctions.matchesType(self.snippet.type(), types, [field.type.toUpperCase()]) ||
-              sqlFunctions.matchesType(self.snippet.type(), [column.type.toUpperCase()], types)) {
-            completions.push({value: self.backTickIfNeeded(field.name), meta: field.type, type: 'column'});
-          }
-        });
-      } else if (data.type === 'array' && (data.item && data.item.fields)) {
-        data.item.fields.forEach(function (field) {
-          if ((field.type === 'array' || field.type === 'map')) {
-            if (self.snippet.type() === 'hive') {
-              completions.push({value: self.backTickIfNeeded(field.name) + '[]', meta: field.type, type: 'column'});
-            } else {
+    } else {
+      var callback = function (data) {
+        if (data.extended_columns) {
+          data.extended_columns.forEach(function (column) {
+            if (column.type.indexOf('map') === 0 && self.snippet.type() === 'hive') {
+              completions.push({value: self.backTickIfNeeded(column.name) + '[]', meta: 'map', type: 'column'})
+            } else if (column.type.indexOf('map') === 0) {
+              completions.push({value: self.backTickIfNeeded(column.name), meta: 'map', type: 'column'})
+            } else if (column.type.indexOf('struct') === 0) {
+              completions.push({value: self.backTickIfNeeded(column.name), meta: 'struct', type: 'column'})
+            } else if (column.type.indexOf('array') === 0 && self.snippet.type() === 'hive') {
+              completions.push({value: self.backTickIfNeeded(column.name) + '[]', meta: 'array', type: 'column'})
+            } else if (column.type.indexOf('array') === 0) {
+              completions.push({value: self.backTickIfNeeded(column.name), meta: 'array', type: 'column'})
+            } else if (sqlFunctions.matchesType(self.snippet.type(), types, [column.type.toUpperCase()]) ||
+                sqlFunctions.matchesType(self.snippet.type(), [column.type.toUpperCase()], types)) {
+              completions.push({value: self.backTickIfNeeded(column.name), meta: column.type, type: 'column'})
+            }
+          });
+        } else if (data.columns) {
+          data.columns.forEach(function (column) {
+            completions.push({value: self.backTickIfNeeded(column), meta: 'column', type: 'column'})
+          });
+        }
+        if (data.type === 'map' && self.snippet.type() === 'impala') {
+          completions.push({value: 'key', meta: 'key', type: 'column'});
+          completions.push({value: 'value', meta: 'value', type: 'column'});
+        }
+        if (data.type === 'struct') {
+          data.fields.forEach(function (field) {
+            completions.push({value: self.backTickIfNeeded(field.name), meta: 'struct', type: 'column'})
+          });
+        } else if (data.type === 'map' && (data.value && data.value.fields)) {
+          data.value.fields.forEach(function (field) {
+            if (sqlFunctions.matchesType(self.snippet.type(), types, [field.type.toUpperCase()]) ||
+                sqlFunctions.matchesType(self.snippet.type(), [column.type.toUpperCase()], types)) {
               completions.push({value: self.backTickIfNeeded(field.name), meta: field.type, type: 'column'});
             }
-          } else if (sqlFunctions.matchesType(self.snippet.type(), types, [field.type.toUpperCase()]) ||
-              sqlFunctions.matchesType(self.snippet.type(), [column.type.toUpperCase()], types)) {
-            completions.push({value: self.backTickIfNeeded(field.name), meta: field.type, type: 'column'});
-          }
-        });
-      }
-      addColumnsDeferred.resolve();
-    };
-
-    self.fetchFieldsForIdentifiers(editor, parseResult.suggestColumns.table, parseResult.suggestColumns.database || database, parseResult.suggestColumns.identifierChain, callback, addColumnsDeferred.resolve);
+          });
+        } else if (data.type === 'array' && (data.item && data.item.fields)) {
+          data.item.fields.forEach(function (field) {
+            if ((field.type === 'array' || field.type === 'map')) {
+              if (self.snippet.type() === 'hive') {
+                completions.push({value: self.backTickIfNeeded(field.name) + '[]', meta: field.type, type: 'column'});
+              } else {
+                completions.push({value: self.backTickIfNeeded(field.name), meta: field.type, type: 'column'});
+              }
+            } else if (sqlFunctions.matchesType(self.snippet.type(), types, [field.type.toUpperCase()]) ||
+                sqlFunctions.matchesType(self.snippet.type(), [column.type.toUpperCase()], types)) {
+              completions.push({value: self.backTickIfNeeded(field.name), meta: field.type, type: 'column'});
+            }
+          });
+        }
+        addColumnsDeferred.resolve();
+      };
 
+      self.fetchFieldsForIdentifiers(editor, parseResult.suggestColumns.table, parseResult.suggestColumns.database || database, parseResult.suggestColumns.identifierChain, callback, addColumnsDeferred.resolve);
+    }
     return addColumnsDeferred;
   };
 
@@ -452,6 +489,9 @@
 
   SqlAutocompleter2.prototype.backTickIfNeeded = function (text) {
     var self = this;
+    if (text.indexOf('`') === 0) {
+      return text;
+    }
     var upperText = text.toUpperCase();
     if (self.snippet.type() === 'hive' && (hiveReservedKeywords[upperText] || extraHiveReservedKeywords[upperText])) {
       return '`' + text + '`';

+ 112 - 0
desktop/core/src/desktop/static/desktop/spec/autocomplete/sqlSpec.js

@@ -82,6 +82,118 @@ define([
       });
     });
 
+    describe('Error Handling', function () {
+      it('should suggest keywords for "bla; |"', function() {
+        assertAutoComplete({
+          beforeCursor: 'bla; ',
+          afterCursor: '',
+          containsKeywords: ['SELECT'],
+          expectedResult: {
+            lowerCase: false
+          }
+        });
+      });
+
+      it('should suggest keywords for "bla bla bla;bla; |"', function() {
+        assertAutoComplete({
+          beforeCursor: 'bla bla bla;bla; ',
+          afterCursor: '',
+          containsKeywords: ['SELECT'],
+          expectedResult: {
+            lowerCase: false
+          }
+        });
+      });
+
+      it('should suggest keywords for "Åäö; |"', function() {
+        assertAutoComplete({
+          beforeCursor: 'Åäö; ',
+          afterCursor: '',
+          containsKeywords: ['SELECT'],
+          expectedResult: {
+            lowerCase: false
+          }
+        });
+      });
+
+      it('should suggest keywords for "bla bla bla;bla;\\n|;bladiblaa blaa"', function() {
+        assertAutoComplete({
+          beforeCursor: 'bla bla bla;bla;\n',
+          afterCursor: ';bladiblaa blaa',
+          containsKeywords: ['SELECT'],
+          expectedResult: {
+            lowerCase: false
+          }
+        });
+      });
+
+      it('should suggest keywords for "FROM; |"', function() {
+        assertAutoComplete({
+          beforeCursor: 'FROM; ',
+          afterCursor: '',
+          containsKeywords: ['SELECT'],
+          expectedResult: {
+            lowerCase: false
+          }
+        });
+      });
+
+      it('should suggest keywords for "FROM USE; |"', function() {
+        assertAutoComplete({
+          beforeCursor: 'FROM USE; ',
+          afterCursor: '',
+          containsKeywords: ['SELECT'],
+          expectedResult: {
+            lowerCase: false
+          }
+        });
+      });
+
+      it('should suggest keywords for "FROM SELECT; OR FROM FROM; |"', function() {
+        assertAutoComplete({
+          beforeCursor: 'FROM SELECT; OR FROM FROM;',
+          afterCursor: '',
+          containsKeywords: ['SELECT'],
+          expectedResult: {
+            lowerCase: false
+          }
+        });
+      });
+
+      it('should suggest keywords for "FROM SELECT; OR FROM FROM; |;BLAAA; AND;"', function() {
+        assertAutoComplete({
+          beforeCursor: 'FROM SELECT; OR FROM FROM;',
+          afterCursor: ';BLAAA; AND;',
+          containsKeywords: ['SELECT'],
+          expectedResult: {
+            lowerCase: false
+          }
+        });
+      });
+      
+      it('should suggest keywords for "FROM bla bla;AND booo; |"', function() {
+        assertAutoComplete({
+          beforeCursor: 'FROM bla bla;AND booo;',
+          afterCursor: '',
+          containsKeywords: ['SELECT'],
+          expectedResult: {
+            lowerCase: false
+          }
+        });
+      });
+
+      it('should suggest keywords for "|; SELECT LIMIT 10"', function() {
+        assertAutoComplete({
+          beforeCursor: '',
+          afterCursor: '; SELECT LIMIT 10',
+          containsKeywords: ['SELECT'],
+          expectedResult: {
+            lowerCase: false
+          }
+        });
+      });
+    });
+
     describe('Impala specific', function () {
       it('should suggest keywords for "|"', function() {
         assertAutoComplete({

+ 540 - 29
desktop/core/src/desktop/static/desktop/spec/autocomplete/sqlSpecSelect.js

@@ -787,18 +787,23 @@ define([
         });
       });
 
-    it('should suggest aliases for "SELECT | FROM testTableA tta, (SELECT SUM(A*B) total FROM tta.array) ttaSum, testTableB ttb"', function() {
+      it('should suggest aliases for "SELECT | FROM testTableA tta, (SELECT SUM(A*B) total FROM tta.array) ttaSum, testTableB ttb"', function() {
         assertAutoComplete({
           beforeCursor: 'SELECT ',
           afterCursor: ' FROM testTableA tta, (SELECT SUM(A*B) total FROM tta.array) ttaSum, testTableB ttb',
           ignoreErrors: true,
           hasLocations: true,
+          dialect: 'hive',
           expectedResult: {
             lowerCase: false,
             suggestKeywords: ['*', 'ALL', 'DISTINCT'],
             suggestAggregateFunctions: true,
             suggestFunctions: {},
-            suggestIdentifiers: [{ name: 'tta.', type: 'alias' }, { name: 'ttaSum.', type: 'subquery' }, { name: 'ttb.', type: 'alias' }]
+            suggestIdentifiers: [{ name: 'tta.', type: 'alias' }, { name: 'ttaSum.', type: 'sub-query' }, { name: 'ttb.', type: 'alias' }],
+            subQueries: [{
+              alias: 'ttaSum',
+              columns: [{ alias: 'total', type: 'DOUBLE' }]
+            }]
           }
         });
       });
@@ -3046,6 +3051,19 @@ define([
         });
       });
 
+      it('should suggest columns for "select foo from tbl where | % 2 = 0"', function() {
+        assertAutoComplete({
+          beforeCursor: 'select foo from tbl where ',
+          afterCursor: ' % 2 = 0',
+          hasLocations: true,
+          expectedResult: {
+            lowerCase: true,
+            suggestFunctions: { types: ['NUMBER'] },
+            suggestColumns: { types: ['NUMBER'], table: 'tbl' }
+          }
+        });
+      });
+
       it('should suggest values for "SELECT * FROM testTable WHERE -id = |"', function() {
         assertAutoComplete({
           beforeCursor: 'SELECT * FROM testTable WHERE -id = ',
@@ -3798,6 +3816,43 @@ define([
         });
       });
 
+      it('should suggest columns for "SELECT * FROM foo WHERE id LIKE |"', function () {
+        assertAutoComplete({
+          beforeCursor: 'SELECT * FROM foo WHERE id LIKE ',
+          afterCursor: '',
+          hasLocations: true,
+          expectedResult: {
+            lowerCase: false,
+            suggestFunctions: { types: ['STRING'] },
+            suggestColumns: { types: ['STRING'], table: 'foo' }
+          }
+        });
+      });
+
+      it('should suggest keywords for "SELECT * FROM foo WHERE id LIKE \'\' GROUP |"', function () {
+        assertAutoComplete({
+          beforeCursor: 'SELECT * FROM foo WHERE id LIKE \'\' GROUP ',
+          afterCursor: '',
+          hasLocations: true,
+          expectedResult: {
+            lowerCase: false,
+            suggestKeywords: ['BY']
+          }
+        });
+      });
+
+      it('should suggest keywords for "SELECT * FROM foo WHERE id LIKE (\'bla bla\') |"', function () {
+        assertAutoComplete({
+          beforeCursor: 'SELECT * FROM foo WHERE id LIKE (\'bla bla\') ',
+          afterCursor: '',
+          hasLocations: true,
+          containsKeywords: ['AND'],
+          expectedResult: {
+            lowerCase: false
+          }
+        });
+      });
+
       it('should suggest identifiers for "SELECT * FROM foo bla, bar WHERE id IS NULL AND |"', function () {
         assertAutoComplete({
           beforeCursor: 'SELECT * FROM foo bla, bar WHERE id IS NULL AND ',
@@ -3819,7 +3874,8 @@ define([
           expectedResult: {
             lowerCase: false,
             suggestFunctions: {},
-            suggestColumns: { table: 'foo' }
+            suggestColumns: { table: 'foo' },
+            suggestIdentifiers: [{ name: 'bla.', type: 'alias' }]
           }
         });
       });
@@ -3832,7 +3888,8 @@ define([
           expectedResult: {
             lowerCase: false,
             suggestFunctions: {},
-            suggestColumns: { table: 'foo' }
+            suggestColumns: { table: 'foo' },
+            suggestIdentifiers: [{ name: 'bla.', type: 'alias' }]
           }
         });
       });
@@ -3845,7 +3902,8 @@ define([
           expectedResult: {
             lowerCase: false,
             suggestFunctions: {},
-            suggestColumns: { table: 'foo' }
+            suggestColumns: { table: 'foo' },
+            suggestIdentifiers: [{ name: 'bla.', type: 'alias' }]
           }
         });
       });
@@ -3859,7 +3917,8 @@ define([
             lowerCase: false,
             suggestFunctions: {},
             suggestColumns: { table: 'foo' },
-            suggestKeywords: ['EXISTS']
+            suggestKeywords: ['EXISTS'],
+            suggestIdentifiers: [{ name: 'bar.', type: 'alias' }]
           }
         });
       });
@@ -3872,7 +3931,8 @@ define([
           expectedResult: {
             lowerCase: false,
             suggestFunctions: { types: ['BOOLEAN'] },
-            suggestColumns: { types: ['BOOLEAN'], table: 'foo' }
+            suggestColumns: { types: ['BOOLEAN'], table: 'foo' },
+            suggestIdentifiers: [{ name: 'bar.', type: 'alias' }]
           }
         });
       });
@@ -3885,7 +3945,8 @@ define([
           expectedResult: {
             lowerCase: false,
             suggestFunctions: { types: ['BOOLEAN'] },
-            suggestColumns: { types: ['BOOLEAN'], table: 'foo' }
+            suggestColumns: { types: ['BOOLEAN'], table: 'foo' },
+            suggestIdentifiers: [{ name: 'bar.', type: 'alias' }]
           }
         });
       });
@@ -4471,6 +4532,96 @@ define([
         });
       });
 
+      it('should suggest keywords for "SELECT * FROM testTable1 INNER |"', function() {
+        assertAutoComplete({
+          beforeCursor: 'SELECT * FROM testTable1 INNER ',
+          afterCursor: '',
+          hasLocations: true,
+          dialect: 'generic',
+          expectedResult: {
+            lowerCase: false,
+            suggestKeywords: [ 'JOIN' ]
+          }
+        });
+      });
+
+      it('should suggest keywords for "SELECT * FROM testTable1 FULL |"', function() {
+        assertAutoComplete({
+          beforeCursor: 'SELECT * FROM testTable1 FULL ',
+          afterCursor: '',
+          hasLocations: true,
+          dialect: 'generic',
+          expectedResult: {
+            lowerCase: false,
+            suggestKeywords: [ 'JOIN', 'OUTER JOIN' ]
+          }
+        });
+      });
+
+      it('should suggest keywords for "SELECT * FROM testTable1 FULL OUTER |"', function() {
+        assertAutoComplete({
+          beforeCursor: 'SELECT * FROM testTable1 FULL OUTER ',
+          afterCursor: '',
+          hasLocations: true,
+          dialect: 'generic',
+          expectedResult: {
+            lowerCase: false,
+            suggestKeywords: [ 'JOIN' ]
+          }
+        });
+      });
+
+      it('should suggest keywords for "SELECT * FROM testTable1 LEFT |"', function() {
+        assertAutoComplete({
+          beforeCursor: 'SELECT * FROM testTable1 LEFT ',
+          afterCursor: '',
+          hasLocations: true,
+          dialect: 'generic',
+          expectedResult: {
+            lowerCase: false,
+            suggestKeywords: [ 'JOIN', 'OUTER JOIN' ]
+          }
+        });
+      });
+
+      it('should suggest keywords for "SELECT * FROM testTable1 LEFT OUTER |"', function() {
+        assertAutoComplete({
+          beforeCursor: 'SELECT * FROM testTable1 LEFT OUTER ',
+          afterCursor: '',
+          hasLocations: true,
+          expectedResult: {
+            lowerCase: false,
+            suggestKeywords: [ 'JOIN' ]
+          }
+        });
+      });
+
+      it('should suggest keywords for "SELECT * FROM testTable1 RIGHT |"', function() {
+        assertAutoComplete({
+          beforeCursor: 'SELECT * FROM testTable1 RIGHT ',
+          afterCursor: '',
+          hasLocations: true,
+          dialect: 'generic',
+          expectedResult: {
+            lowerCase: false,
+            suggestKeywords: [ 'JOIN', 'OUTER JOIN' ]
+          }
+        });
+      });
+
+      it('should suggest keywords for "SELECT * FROM testTable1 RIGHT OUTER |"', function() {
+        assertAutoComplete({
+          beforeCursor: 'SELECT * FROM testTable1 RIGHT OUTER ',
+          afterCursor: '',
+          hasLocations: true,
+          dialect: 'generic',
+          expectedResult: {
+            lowerCase: false,
+            suggestKeywords: [ 'JOIN' ]
+          }
+        });
+      });
+
       it('should suggest tables for "SELECT * FROM testTable1 JOIN db1.|"', function() {
         assertAutoComplete({
           beforeCursor: 'SELECT * FROM testTable1 JOIN db1.',
@@ -4752,6 +4903,71 @@ define([
           });
         });
 
+        it('should suggest keywords for "SELECT * FROM testTable1 INNER |"', function() {
+          assertAutoComplete({
+            beforeCursor: 'SELECT * FROM testTable1 INNER ',
+            afterCursor: '',
+            hasLocations: true,
+            dialect: 'hive',
+            expectedResult: {
+              lowerCase: false,
+              suggestKeywords: [ 'JOIN' ]
+            }
+          });
+        });
+
+        it('should suggest keywords for "SELECT * FROM testTable1 FULL |"', function() {
+          assertAutoComplete({
+            beforeCursor: 'SELECT * FROM testTable1 FULL ',
+            afterCursor: '',
+            hasLocations: true,
+            dialect: 'hive',
+            expectedResult: {
+              lowerCase: false,
+              suggestKeywords: [ 'JOIN', 'OUTER JOIN' ]
+            }
+          });
+        });
+
+        it('should suggest keywords for "SELECT * FROM testTable1 LEFT |"', function() {
+          assertAutoComplete({
+            beforeCursor: 'SELECT * FROM testTable1 LEFT ',
+            afterCursor: '',
+            hasLocations: true,
+            dialect: 'hive',
+            expectedResult: {
+              lowerCase: false,
+              suggestKeywords: [ 'JOIN', 'OUTER JOIN', 'SEMI JOIN' ]
+            }
+          });
+        });
+
+        it('should suggest keywords for "SELECT * FROM testTable1 RIGHT |"', function() {
+          assertAutoComplete({
+            beforeCursor: 'SELECT * FROM testTable1 RIGHT ',
+            afterCursor: '',
+            hasLocations: true,
+            dialect: 'hive',
+            expectedResult: {
+              lowerCase: false,
+              suggestKeywords: [ 'JOIN', 'OUTER JOIN' ]
+            }
+          });
+        });
+
+        it('should suggest keywords for "SELECT * FROM testTable1 CROSS |"', function() {
+          assertAutoComplete({
+            beforeCursor: 'SELECT * FROM testTable1 CROSS ',
+            afterCursor: '',
+            hasLocations: true,
+            dialect: 'hive',
+            expectedResult: {
+              lowerCase: false,
+              suggestKeywords: [ 'JOIN' ]
+            }
+          });
+        });
+
         it('should suggest keywords for "SELECT t1.* FROM table1 t1 | JOIN"', function () {
           assertAutoComplete({
             beforeCursor: 'SELECT t1.* FROM table1 t1 ',
@@ -4862,6 +5078,58 @@ define([
           });
         });
 
+        it('should suggest keywords for "SELECT * FROM testTable1 INNER |"', function() {
+          assertAutoComplete({
+            beforeCursor: 'SELECT * FROM testTable1 INNER ',
+            afterCursor: '',
+            hasLocations: true,
+            dialect: 'impala',
+            expectedResult: {
+              lowerCase: false,
+              suggestKeywords: [ 'JOIN' ]
+            }
+          });
+        });
+
+        it('should suggest keywords for "SELECT * FROM testTable1 FULL |"', function() {
+          assertAutoComplete({
+            beforeCursor: 'SELECT * FROM testTable1 FULL ',
+            afterCursor: '',
+            hasLocations: true,
+            dialect: 'impala',
+            expectedResult: {
+              lowerCase: false,
+              suggestKeywords: [ 'JOIN', 'OUTER JOIN' ]
+            }
+          });
+        });
+
+        it('should suggest keywords for "SELECT * FROM testTable1 LEFT |"', function() {
+          assertAutoComplete({
+            beforeCursor: 'SELECT * FROM testTable1 LEFT ',
+            afterCursor: '',
+            hasLocations: true,
+            dialect: 'impala',
+            expectedResult: {
+              lowerCase: false,
+              suggestKeywords: [ 'ANTI JOIN', 'JOIN', 'OUTER JOIN', 'SEMI JOIN' ]
+            }
+          });
+        });
+
+        it('should suggest keywords for "SELECT * FROM testTable1 RIGHT |"', function() {
+          assertAutoComplete({
+            beforeCursor: 'SELECT * FROM testTable1 LEFT ',
+            afterCursor: '',
+            hasLocations: true,
+            dialect: 'impala',
+            expectedResult: {
+              lowerCase: false,
+              suggestKeywords: [ 'ANTI JOIN', 'JOIN', 'OUTER JOIN', 'SEMI JOIN' ]
+            }
+          });
+        });
+
         it('should suggest tables for "SELECT * FROM testTable1 JOIN |"', function() {
           assertAutoComplete({
             beforeCursor: 'SELECT * FROM testTable1 JOIN ',
@@ -4973,7 +5241,7 @@ define([
       });
     });
 
-    describe('Subqueries in WHERE Clause', function () {
+    describe('SubQueries in WHERE Clause', function () {
       it('should suggest keywords for "SELECT * FROM foo WHERE bar |"', function() {
         assertAutoComplete({
           beforeCursor: 'SELECT * FROM foo WHERE bar ',
@@ -5102,7 +5370,7 @@ define([
       });
     });
 
-    describe('Subqueries in FROM Clause', function () {
+    describe('SubQueries in FROM Clause', function () {
       it('should suggest keywords for "SELECT * FROM (|"', function() {
         assertAutoComplete({
           beforeCursor: 'SELECT * FROM (',
@@ -5147,19 +5415,6 @@ define([
         });
       });
 
-      it('should suggest columns for "select foo from tbl where | % 2 = 0"', function() {
-        assertAutoComplete({
-          beforeCursor: 'select foo from tbl where ',
-          afterCursor: ' % 2 = 0',
-          hasLocations: true,
-          expectedResult: {
-            lowerCase: true,
-            suggestFunctions: { types: ['NUMBER'] },
-            suggestColumns: { types: ['NUMBER'], table: 'tbl' }
-          }
-        });
-      });
-
       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"', function() {
         assertAutoComplete({
           beforeCursor: 'SELECT "contains an even number" FROM t1, t2 AS ta2 WHERE EXISTS (SELECT t3.foo FROM t3 WHERE ',
@@ -5187,17 +5442,112 @@ define([
             suggestKeywords: ['*', 'ALL', 'DISTINCT'],
             suggestAggregateFunctions: true,
             suggestFunctions: {},
-            suggestIdentifiers: [{ name: 'tt.', type: 'alias'}, { name: 'bar.', type: 'subquery'}],
+            suggestIdentifiers: [{ name: 'tt.', type: 'alias'}, { name: 'bar.', type: 'sub-query'}],
             locations: [
               {type: 'table', location: { first_line: 1, last_line: 1, first_column: 14, last_column: 23}, table: 'testTable'},
               {type: 'column', location: { first_line: 1, last_line: 1, first_column: 36, last_column: 39}, identifierChain: [{ name: 'bla'}], table: 'abc'},
               {type: 'table', location: { first_line: 1, last_line: 1, first_column: 45, last_column: 48}, table: 'abc'},
               {type: 'column', location: { first_line: 1, last_line: 1, first_column: 55, last_column: 58}, identifierChain: [{ name: 'foo'}], table: 'abc'}
+            ],
+            subQueries: [{
+              alias: 'bar',
+              columns: [
+                { identifierChain: [{ name: 'bla' }], type: 'COLREF', table: 'abc'}
+              ]
+            }]
+          }
+        });
+      });
+
+      it('should suggest columns for "select | from (select id i, name as n, bla from foo) bar"', function() {
+        assertAutoComplete({
+          beforeCursor: 'select ',
+          afterCursor: ' from (select id i, name as n, bla from foo) bar',
+          hasLocations: true,
+          expectedResult: {
+            lowerCase: true,
+            suggestKeywords: ['*', 'ALL', 'DISTINCT'],
+            suggestAggregateFunctions: true,
+            suggestFunctions: {},
+            suggestColumns: { subQuery: 'bar' },
+            subQueries: [{
+              alias: 'bar',
+              columns: [
+                { alias: 'i', identifierChain: [{ name: 'id' }], type: 'COLREF', table: 'foo' },
+                { alias: 'n', identifierChain: [{ name: 'name' }], type: 'COLREF', table: 'foo' },
+                { identifierChain: [{ name: 'bla' }], type: 'COLREF', table: 'foo' }
+              ]
+            }],
+            suggestIdentifiers: [{ name: 'bar.', type: 'sub-query' }]
+          }
+        });
+      });
+
+      it('should suggest columns for "select | from (select id i, name as n, bla from foo) bar"', function() {
+        assertAutoComplete({
+          beforeCursor: 'select ',
+          afterCursor: ' from (select id i, name as n, bla from foo) bar',
+          hasLocations: true,
+          expectedResult: {
+            lowerCase: true,
+            suggestKeywords: ['*', 'ALL', 'DISTINCT'],
+            suggestAggregateFunctions: true,
+            suggestFunctions: {},
+            suggestColumns: { subQuery: 'bar' },
+            subQueries: [{
+              alias: 'bar',
+              columns: [
+                { alias: 'i', identifierChain: [{ name: 'id' }], type: 'COLREF', table: 'foo' },
+                { alias: 'n', identifierChain: [{ name: 'name' }], type: 'COLREF', table: 'foo' },
+                { identifierChain: [{ name: 'bla' }], type: 'COLREF', table: 'foo' }
+              ]
+            }],
+            suggestIdentifiers: [{ name: 'bar.', type: 'sub-query' }]
+          }
+        });
+      });
+
+      it('should suggest sub-query columns for "SELECT bar.| FROM (SELECT col1, col2, (col3 + 1) col3alias FROM foo) bar"', function() {
+        assertAutoComplete({
+          beforeCursor: 'SELECT bar.',
+          afterCursor: ' FROM (SELECT col1, col2, (col3 + 1) col3alias FROM foo) bar',
+          hasLocations: true,
+          expectedResult: {
+            lowerCase: false,
+            suggestKeywords: ['*'],
+            suggestColumns: { subQuery: 'bar' },
+            subQueries: [{
+              alias: 'bar',
+              columns: [
+                { identifierChain: [{ name: 'col1' }], type: 'COLREF', table: 'foo' },
+                { identifierChain: [{ name: 'col2' }], type: 'COLREF', table: 'foo' },
+                { alias: 'col3alias', type: 'NUMBER' }
+              ]}
             ]
           }
         });
       });
 
+      it('should suggest sub-query columns for "SELECT bar.| FROM (SELECT b FROM foo) boo, (SELECT a FROM bla) bar"', function() {
+        assertAutoComplete({
+          beforeCursor: 'SELECT bar.',
+          afterCursor: ' FROM (SELECT b FROM foo) boo, (SELECT a FROM bla) bar',
+          hasLocations: true,
+          expectedResult: {
+            lowerCase: false,
+            suggestKeywords: ['*'],
+            suggestColumns: { subQuery: 'bar' },
+            subQueries: [{
+              alias: 'boo',
+              columns: [{ identifierChain: [{ name: 'b' }], type: 'COLREF', table: 'foo' }]
+            }, {
+              alias: 'bar',
+              columns: [{ identifierChain: [{ name: 'a' }], type: 'COLREF', table: 'bla' }]
+            }]
+          }
+        });
+      });
+
       it('should suggest tables for "SELECT * FROM (SELECT |)"', function() {
         assertAutoComplete({
           beforeCursor: 'SELECT * FROM (SELECT ',
@@ -5220,25 +5570,32 @@ define([
         });
       });
 
-      it('should suggest identifiers for "SELECT | FROM (SELECT * FROM tableOne) AS subqueryOne, someDb.tableTwo tAlias, tableThree, (SELECT * FROM t3 JOIN t4 ON t3.id = t4.id) subqueryTwo;"', function() {
+      it('should suggest identifiers for "SELECT | FROM (SELECT * FROM tableOne) AS subQueryOne, someDb.tableTwo tAlias, tableThree, (SELECT * FROM t3 JOIN table4 t4 ON t3.id = t4.id) subQueryTwo;"', function() {
         assertAutoComplete({
           beforeCursor: 'SELECT ',
-          afterCursor: ' FROM (SELECT * FROM tableOne) AS subqueryOne, someDb.tableTwo tAlias, tableThree, (SELECT * FROM t3 JOIN t4 ON t3.id = t4.id) subqueryTwo;',
+          afterCursor: ' FROM (SELECT * FROM tableOne) AS subQueryOne, someDb.tableTwo tAlias, tableThree, (SELECT * FROM t3 JOIN table4 t4 ON t3.id = t4.id) subQueryTwo;',
           hasLocations: true,
           expectedResult: {
             lowerCase: false,
             suggestKeywords: ['*', 'ALL', 'DISTINCT'],
             suggestAggregateFunctions: true,
             suggestFunctions: {},
-            suggestIdentifiers: [{ name: 'subqueryOne.', type: 'subquery'}, { name: 'tAlias.', type: 'alias'}, { name: 'tableThree.', type: 'table'}, { name: 'subqueryTwo.', type: 'subquery'}]
+            suggestIdentifiers: [{ name: 'subQueryOne.', type: 'sub-query'}, { name: 'tAlias.', type: 'alias'}, { name: 'tableThree.', type: 'table'}, { name: 'subQueryTwo.', type: 'sub-query'}],
+            subQueries: [{
+              alias: 'subQueryOne',
+              columns: [{ table: 'tableOne' }]
+            }, {
+              alias: 'subQueryTwo',
+              columns: [{ tables: [{ table: 't3' }, { table: 'table4' }] }]
+            }]
           }
         });
       });
 
-      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;"', function() {
+      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;"', function() {
         assertAutoComplete({
           beforeCursor: 'SELECT * FROM (SELECT ',
-          afterCursor: ' FROM tableOne) subqueryOne, someDb.tableTwo talias, (SELECT * FROM t3 JOIN t4 ON t3.id = t4.id) AS subqueryTwo;',
+          afterCursor: ' FROM tableOne) subQueryOne, someDb.tableTwo talias, (SELECT * FROM t3 JOIN t4 ON t3.id = t4.id) AS subQueryTwo;',
           hasLocations: true,
           expectedResult: {
             lowerCase: false,
@@ -5251,6 +5608,160 @@ define([
           }
         });
       });
+
+      it('should suggest columns for "SELECT | FROM (SELECT * FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo"', function () {
+        assertAutoComplete({
+          beforeCursor: 'SELECT ',
+          afterCursor: ' FROM (SELECT * FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo',
+          hasLocations: true,
+          expectedResult: {
+            lowerCase:false,
+            suggestKeywords: ['*', 'ALL', 'DISTINCT'],
+            suggestAggregateFunctions: true,
+            suggestFunctions: {},
+            suggestColumns: { subQuery: 'subQueryTwo'},
+            subQueries: [{
+              alias: 'subQueryTwo',
+              columns: [{ subQuery: 'subQueryOne' }],
+              subQueries: [{
+                alias: 'subQueryOne',
+                columns: [{ table: 'tableOne'}]
+              }]
+            }],
+            suggestIdentifiers: [{ name: 'subQueryTwo.', type: 'sub-query' }]
+          }
+        });
+      });
+
+      it('should suggest columns for "SELECT | FROM (SELECT * FROM (SELECT * FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo) subQueryThree"', function () {
+        assertAutoComplete({
+          beforeCursor: 'SELECT ',
+          afterCursor: ' FROM (SELECT * FROM (SELECT * FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo) subQueryThree',
+          hasLocations: true,
+          expectedResult: {
+            lowerCase:false,
+            suggestKeywords: ['*', 'ALL', 'DISTINCT'],
+            suggestAggregateFunctions: true,
+            suggestFunctions: {},
+            suggestColumns: { subQuery: 'subQueryThree'},
+            subQueries: [{
+              alias: 'subQueryThree',
+              columns: [{ subQuery: 'subQueryTwo' }],
+              subQueries: [{
+                alias: 'subQueryTwo',
+                columns: [{ subQuery: 'subQueryOne' }],
+                subQueries: [{
+                  alias: 'subQueryOne',
+                  columns: [{ table: 'tableOne' }]
+                }]
+              }]
+            }],
+            suggestIdentifiers: [{ name: 'subQueryThree.', type: 'sub-query' }]
+          }
+        });
+      });
+
+      it('should suggest columns for "SELECT * FROM (SELECT | FROM (SELECT * FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo) subQueryThree"', function () {
+        assertAutoComplete({
+          beforeCursor: 'SELECT * FROM (SELECT ',
+          afterCursor: ' FROM (SELECT * FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo) subQueryThree',
+          hasLocations: true,
+          expectedResult: {
+            lowerCase:false,
+            suggestKeywords: ['*', 'ALL', 'DISTINCT'],
+            suggestAggregateFunctions: true,
+            suggestFunctions: {},
+            suggestColumns: { subQuery: 'subQueryTwo'},
+            subQueries: [{
+              alias: 'subQueryTwo',
+              columns: [{ subQuery: 'subQueryOne' }],
+              subQueries: [{
+                alias: 'subQueryOne',
+                columns: [{ table: 'tableOne' }]
+              }]
+            }],
+            suggestIdentifiers: [{ name: 'subQueryTwo.', type: 'sub-query' }]
+          }
+        });
+      });
+
+      it('should suggest columns for "SELECT * FROM (SELECT * FROM (SELECT | FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo) subQueryThree"', function () {
+        assertAutoComplete({
+          beforeCursor: 'SELECT * FROM (SELECT * FROM (SELECT ',
+          afterCursor: ' FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo) subQueryThree',
+          hasLocations: true,
+          expectedResult: {
+            lowerCase:false,
+            suggestKeywords: ['*', 'ALL', 'DISTINCT'],
+            suggestAggregateFunctions: true,
+            suggestFunctions: {},
+            suggestColumns: { subQuery: 'subQueryOne'},
+            subQueries: [{
+              alias: 'subQueryOne',
+              columns: [{ table: 'tableOne' }]
+            }],
+            suggestIdentifiers: [{ name: 'subQueryOne.', type: 'sub-query' }]
+          }
+        });
+      });
+
+      it('should suggest columns for "SELECT s2.| FROM (SELECT a, bla FROM (SELECT a, b, abs(1) as bla FROM testTable) s1) s2;"', function () {
+        assertAutoComplete({
+          beforeCursor: 'SELECT s2.',
+          afterCursor: ' FROM (SELECT a, bla FROM (SELECT a, b, abs(1) as bla FROM testTable) s1) s2;',
+          hasLocations: true,
+          dialect: 'generic',
+          expectedResult: {
+            lowerCase:false,
+            suggestKeywords: ['*'],
+            suggestColumns: { subQuery: 's2' },
+            subQueries: [{
+              alias: 's2',
+              columns: [
+                { identifierChain: [{ name: 'a' }], type: 'COLREF', subQuery: 's1'},
+                { identifierChain: [{ name: 'bla' }], type: 'COLREF', subQuery: 's1'}
+              ],
+              subQueries: [{
+                alias: 's1',
+                columns: [
+                  { identifierChain: [{ name: 'a' }], type: 'COLREF', table: 'testTable'},
+                  { identifierChain: [{ name: 'b' }], type: 'COLREF', table: 'testTable'},
+                  { alias: 'bla', type: 'T'}
+                ]
+              }]
+            }]
+          }
+        });
+      });
+
+      it('should suggest columns for "SELECT s2.| FROM (SELECT a, bla FROM (SELECT a, b, abs(1) as bla FROM testTable) s1) s2;"', function () {
+        assertAutoComplete({
+          beforeCursor: 'SELECT s2.',
+          afterCursor: ' FROM (SELECT a, bla FROM (SELECT a, b, abs(1) as bla FROM testTable) s1) s2;',
+          hasLocations: true,
+          dialect: 'hive',
+          expectedResult: {
+            lowerCase:false,
+            suggestKeywords: ['*'],
+            suggestColumns: { subQuery: 's2' },
+            subQueries: [{
+              alias: 's2',
+              columns: [
+                { identifierChain: [{ name: 'a' }], type: 'COLREF', subQuery: 's1'},
+                { identifierChain: [{ name: 'bla' }], type: 'COLREF', subQuery: 's1'}
+              ],
+              subQueries: [{
+                alias: 's1',
+                columns: [
+                  { identifierChain: [{ name: 'a' }], type: 'COLREF', table: 'testTable'},
+                  { identifierChain: [{ name: 'b' }], type: 'COLREF', table: 'testTable'},
+                  { alias: 'bla', type: 'DOUBLE'}
+                ]
+              }]
+            }]
+          }
+        });
+      });
     });
   });
 });

+ 5 - 0
desktop/core/src/desktop/static/desktop/spec/sqlAutocompleter2Spec.js

@@ -77,5 +77,10 @@ define([
       expect(hiveSubject.backTickIfNeeded('Kada')).toEqual('Kada');
     });
 
+    it('should not backtick identifiers that are backticked', function () {
+      // [A-Za-z][A-Za-z0-9_]*
+      expect(hiveSubject.backTickIfNeeded('`bla bla`')).toEqual('`bla bla`');
+    });
+
   });
 });

部分文件因为文件数量过多而无法显示