浏览代码

[core] Proper support for JOIN in autocomplete

This commit adds support for autocompletion related to table joins. It also takes care of an issue with table autocompletion in parentheses and some other minor related bugs.
Johan Ahlen 10 年之前
父节点
当前提交
576e387

+ 25 - 21
desktop/core/src/desktop/static/desktop/js/autocomplete.js

@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-var SQL_TERMS = /\b(FROM|TABLE|STATS|REFRESH|METADATA|DESCRIBE|ORDER BY|ON|WHERE|SELECT|LIMIT|GROUP|SORT)\b/g;
+var SQL_TERMS = /\b(FROM|TABLE|STATS|REFRESH|METADATA|DESCRIBE|ORDER BY|JOIN|ON|WHERE|SELECT|LIMIT|GROUP|SORT)\b/g;
 var TIME_TO_LIVE_IN_MILLIS = 86400000; // 1 day
 
 /**
@@ -49,7 +49,7 @@ Autocompleter.prototype.getTableReferenceIndex = function (statement) {
     if (upToMatch) {
       tableRefsRaw = $.trim(tableRefsRaw.substring(0, upToMatch.index));
     }
-    var tableRefs = tableRefsRaw.split(",");
+    var tableRefs = tableRefsRaw.split(/\s*(?:,|\bJOIN\b)\s*/i);
     tableRefs.sort();
     $.each(tableRefs, function(index, tableRefRaw) {
       var tableMatch = tableRefRaw.match(/ *([^ ]*) ?([^ ]*)? */);
@@ -195,24 +195,28 @@ Autocompleter.prototype.autocomplete = function(beforeCursor, afterCursor, callb
   var beforeMatcher = beforeCursorU.match(SQL_TERMS);
   var afterMatcher = afterCursorU.match(SQL_TERMS);
 
-  var tableNameAutoComplete = beforeMatcher != null && (
-    beforeMatcher[beforeMatcher.length - 1] === "FROM" ||
-    beforeMatcher[beforeMatcher.length - 1] === "TABLE" ||
-    beforeMatcher[beforeMatcher.length - 1] === "STATS" ||
-    beforeMatcher[beforeMatcher.length - 1] === "REFRESH" ||
-    beforeMatcher[beforeMatcher.length - 1] === "METADATA" ||
-    beforeMatcher[beforeMatcher.length - 1] === "DESCRIBE");
+  if (beforeMatcher == null || beforeMatcher.length == 0) {
+    callback([]);
+    return;
+  }
+
+  var keywordBeforeCursor = beforeMatcher[beforeMatcher.length - 1];
+
+  var tableNameAutoComplete = keywordBeforeCursor === "FROM" ||
+    keywordBeforeCursor === "TABLE" ||
+    keywordBeforeCursor === "STATS" ||
+    keywordBeforeCursor === "JOIN" ||
+    keywordBeforeCursor === "REFRESH" ||
+    keywordBeforeCursor === "METADATA" ||
+    keywordBeforeCursor === "DESCRIBE";
 
-  var selectBefore = beforeMatcher != null &&
-    beforeMatcher[beforeMatcher.length - 1] === "SELECT";
+  var selectBefore = keywordBeforeCursor === "SELECT";
 
-  var fromAfter = afterMatcher != null &&
-    afterMatcher[0] === "FROM";
+  var fieldTermBefore = keywordBeforeCursor === "WHERE" ||
+    keywordBeforeCursor === "ON" ||
+    keywordBeforeCursor === "ORDER BY";
 
-  var fieldTermBefore = beforeMatcher != null && (
-    beforeMatcher[beforeMatcher.length - 1] === "WHERE" ||
-    beforeMatcher[beforeMatcher.length - 1] === "ON" ||
-    beforeMatcher[beforeMatcher.length - 1] === "ORDER BY");
+  var fromAfter = afterMatcher != null && afterMatcher[0] === "FROM";
 
   if (tableNameAutoComplete || (selectBefore && !fromAfter)) {
     self.assistHelper.fetchTables(function(data) {
@@ -234,7 +238,7 @@ Autocompleter.prototype.autocomplete = function(beforeCursor, afterCursor, callb
       callback(self.extractFields(data, fromKeyword));
     }, onFailure );
   } else if ((selectBefore && fromAfter) || fieldTermBefore) {
-    var partialTermsMatch = beforeCursor.match(/([^ \-\+\<\>\,]*)$/);
+    var partialTermsMatch = beforeCursor.match(/([^ \(\-\+\<\>\,]*)$/);
     var parts = partialTermsMatch ? partialTermsMatch[0].split(".") : [];
 
     if (parts.length > 0 && parts[parts.length - 1] != '') {
@@ -248,13 +252,13 @@ Autocompleter.prototype.autocomplete = function(beforeCursor, afterCursor, callb
       // SELECT tablename.column.
       tableName = tableReferences[parts[0]];
       parts.shift();
-    } else if (Object.keys(tableReferences).length == 1) {
+    } else if (parts.length > 0 && Object.keys(tableReferences).length == 1) {
       // SELECT column.
       // We use first and only table reference
       tableName = tableReferences[Object.keys(tableReferences)[0]];
-    } else if (Object.keys(tableReferences).length > 1) {
+    } else if (Object.keys(tableReferences).length > 0) {
       callback($.map(Object.keys(tableReferences), function(key, idx) {
-        return { value: key + ".", score: 1000 - idx, meta: tableReferences[key] == key ? 'table' : 'alias' };
+        return { value: key + (afterCursor.indexOf(".") == 0 ? "" : "."), score: 1000 - idx, meta: tableReferences[key] == key ? 'table' : 'alias' };
       }));
       return;
     } else {

+ 100 - 0
desktop/core/src/desktop/static/desktop/spec/autocompleteSpec.js

@@ -92,6 +92,24 @@ describe("autocomplete.js", function() {
     expect(callback).toHaveBeenCalled();
   };
 
+  it("should return empty suggestions for empty statement", function() {
+    assertAutoComplete({
+      serverResponses: { },
+      beforeCursor: "",
+      afterCursor: "",
+      expectedSuggestions: []
+    });
+  });
+
+  it("should return empty suggestions for bogus statement", function() {
+    assertAutoComplete({
+      serverResponses: { },
+      beforeCursor: "foo",
+      afterCursor: "bar",
+      expectedSuggestions: []
+    });
+  });
+
   describe("table completion", function() {
     it("should suggest table names with no columns", function() {
       assertAutoComplete({
@@ -567,4 +585,86 @@ describe("autocomplete.js", function() {
       });
     });
   });
+
+  describe("joins", function() {
+
+    it("should suggest tables to join with", function() {
+      assertAutoComplete({
+        serverResponses: {
+          "/testApp/api/autocomplete/testDb" : {
+            tables: ["testTable1", "testTable2"]
+          }
+        },
+        beforeCursor: "SELECT * FROM testTable1 JOIN ",
+        afterCursor: "",
+        expectedSuggestions: ["testTable1", "testTable2"]
+      });
+    });
+
+    it("should suggest table references in join condition if not already there", function() {
+      assertAutoComplete({
+        serverResponses: {},
+        beforeCursor: "SELECT testTable1.* FROM testTable1 JOIN testTable2 ON (",
+        afterCursor: "",
+        expectedSuggestions: ["testTable1.", "testTable2."]
+      });
+    });
+
+    it("should suggest table references in join condition if not already there for multiple conditions", function() {
+      assertAutoComplete({
+        serverResponses: {},
+        beforeCursor: "SELECT testTable1.* FROM testTable1 JOIN testTable2 ON (testTable1.testColumn1 = testTable2.testColumn3 AND ",
+        afterCursor: "",
+        expectedSuggestions: ["testTable1.", "testTable2."]
+      });
+    });
+
+    it("should suggest table references in join condition if not already there for multiple conditions", function() {
+      assertAutoComplete({
+        serverResponses: {},
+        beforeCursor: "SELECT testTable1.* FROM testTable1 JOIN testTable2 ON (",
+        afterCursor: " AND testTable1.testColumn1 = testTable2.testColumn3",
+        expectedSuggestions: ["testTable1.", "testTable2."]
+      });
+    });
+
+    it("should suggest field references in join condition if table reference is present", function() {
+      assertAutoComplete({
+        serverResponses: {
+          "/testApp/api/autocomplete/testDb/testTable2" : {
+            columns: ["testColumn3", "testColumn4"]
+          }
+        },
+        beforeCursor: "SELECT testTable1.* FROM testTable1 JOIN testTable2 ON (testTable2.",
+        afterCursor: "",
+        expectedSuggestions: ["testColumn3", "testColumn4"]
+      });
+    });
+
+    it("should suggest field references in join condition if table reference is present from multiple tables", function() {
+      assertAutoComplete({
+        serverResponses: {
+          "/testApp/api/autocomplete/testDb/testTable2" : {
+            columns: ["testColumn3", "testColumn4"]
+          }
+        },
+        beforeCursor: "select * from testTable1 JOIN testTable2 on (testTable1.testColumn1 = testTable2.",
+        afterCursor: "",
+        expectedSuggestions: ["testColumn3", "testColumn4"]
+      });
+    });
+
+    it("should suggest field references in join condition if table reference is present from multiple tables for multiple conditions", function() {
+      assertAutoComplete({
+        serverResponses: {
+          "/testApp/api/autocomplete/testDb/testTable1" : {
+            columns: ["testColumn1", "testColumn2"]
+          }
+        },
+        beforeCursor: "SELECT testTable1.* FROM testTable1 JOIN testTable2 ON (testTable1.testColumn1 = testTable2.testColumn3 AND testTable1.",
+        afterCursor: "",
+        expectedSuggestions: ["testColumn1", "testColumn2"]
+      });
+    });
+  })
 });