Przeglądaj źródła

HUE-5527 [editor] Add autocompleter category for keywords

Johan Ahlen 9 lat temu
rodzic
commit
ed184ae

+ 1 - 1
desktop/core/src/desktop/static/desktop/js/autocompleter.js

@@ -34,7 +34,7 @@ var Autocompleter = (function () {
 
     var initializeAutocompleter = function () {
       if (self.snippet.isSqlDialect() && options.useNewAutocompleter) {
-        self.autocompleter = new SqlAutocompleter2({
+        self.autocompleter = new SqlAutocompleter3({
           snippet: self.snippet,
           timeout: self.timeout
         });

+ 301 - 0
desktop/core/src/desktop/static/desktop/js/sqlAutocompleter3.js

@@ -0,0 +1,301 @@
+// 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.
+
+var SqlAutocompleter3 = (function () {
+
+  // Keyword weights come from the parser
+  var DEFAULT_WEIGHTS = {
+    POPULAR_AGGREGATE: 1500,
+    POPULAR_GROUP_BY: 1400,
+    POPULAR_ORDER_BY: 1300,
+    POPULAR_FILTER: 1200,
+    POPULAR_ACTIVE_JOIN: 1200,
+    POPULAR_JOIN_CONDITION: 1100,
+    COLUMN: 1000,
+    SAMPLE: 900,
+    IDENTIFIER: 800,
+    CTE: 700,
+    TABLE: 600,
+    DATABASE: 500,
+    UDF: 400,
+    HDFS: 300,
+    VIRTUAL_COLUMN: 200,
+    COLREF_KEYWORD: 100,
+    VARIABLE: 50,
+    JOIN: -1
+  };
+
+  var sortByWeight = function (suggestions) {
+    suggestions.sort(function (a, b) {
+      if (typeof a.weight !== 'undefined' && typeof b.weight !== 'undefined' && b.weight !== a.weight) {
+        return b.weight - a.weight;
+      }
+      if (typeof a.weight !== 'undefined' && typeof b.weight === 'undefined') {
+        return -1;
+      }
+      if (typeof b.weight !== 'undefined' && typeof a.weight === 'undefined') {
+        return 1;
+      }
+      return a.value.localeCompare(b.value);
+    })
+  };
+
+  /**
+   * Represents a single suggestion
+   *
+   * @param value
+   * @param meta
+   * @param weight
+   * @constructor
+   */
+  function Suggestion (value, meta, weight) {
+    var self = this;
+    self.value = value;
+    self.meta = meta;
+    self.weight = weight;
+  }
+
+  /**
+   * Represents the keyword category of suggestions
+   *
+   * @param parseResult
+   * @param colRefDeferral
+   * @constructor
+   */
+  function KeywordsCategory (parseResult, colRefDeferral, sourceType) {
+    var self = this;
+    self.label = AutocompleterGlobals.i18n.keywords;
+    self.suggestions = ko.observableArray([]);
+    self.loading = ko.observable(false);
+    self.sourceType = sourceType;
+
+    if (parseResult.suggestKeywords) {
+      var keywordSuggestions = $.map(parseResult.suggestKeywords, function (keyword) {
+        return new Suggestion(parseResult.lowerCase ? keyword.value.toLowerCase() : keyword.value, AutocompleterGlobals.i18n.meta.keyword, keyword.weight);
+      });
+      sortByWeight(keywordSuggestions);
+      self.suggestions(keywordSuggestions);
+    }
+
+    if (parseResult.suggestColRefKeywords) {
+      self.loading(true);
+      // We have to wait for the column reference type to be resolved
+      colRefDeferral.done(function (colRef) {
+        if (colRef !== null) {
+          var keywordSuggestions = self.suggestions();
+          Object.keys(parseResult.suggestColRefKeywords).forEach(function (typeForKeywords) {
+            if (SqlFunctions.matchesType(self.sourceType, [typeForKeywords], [type.toUpperCase()])) {
+              parseResult.suggestColRefKeywords[typeForKeywords].forEach(function (keyword) {
+                keywordSuggestions.push(new Suggestion(parseResult.lowerCase ? keyword.toLowerCase() : keyword, AutocompleterGlobals.i18n.meta.keyword, DEFAULT_WEIGHTS.COLREF_KEYWORD));
+              })
+            }
+          });
+          sortByWeight(keywordSuggestions);
+          self.suggestions(keywordSuggestions);
+        }
+        self.loading(false);
+      });
+    }
+  }
+
+  function Suggestions (options) {
+    var self = this;
+    self.apiHelper = ApiHelper.getInstance();
+    self.parseResult = options.parseResult;
+    self.sourceType = options.sourceType;
+    self.defaultDatabase = options.defaultDatabase;
+    self.timeout = options.timeout;
+    self.callback = options.callback;
+
+    var colRefDeferral = self.handleColumnReference();
+    self.keywords = new KeywordsCategory(options.parseResult, colRefDeferral);
+
+    colRefDeferral.done(self.callback)
+  }
+
+  /**
+   * For some suggestions the column type is needed, for instance with functions we should only suggest
+   * columns that matches the argument type, cos(|) etc.
+   *
+   * The deferred will always resolve, and the default values is { type: 'T' }
+   *
+   * @returns {object} - jQuery Deferred
+   */
+  Suggestions.prototype.handleColumnReference = function () {
+    var self = this;
+    var colRefDeferral = $.Deferred();
+    if (self.parseResult.colRef) {
+      var colRefCallback = function (data) {
+        if (typeof data.type !== 'undefined') {
+          colRefDeferral.resolve(data);
+        } else if (typeof data.extended_columns !== 'undefined' && data.extended_columns.length === 1) {
+          colRefDeferral.resolve(data.extended_columns[0]);
+        }
+      };
+
+      var foundVarRef = self.parseResult.colRef.identifierChain.filter(function (identifier) {
+        return typeof identifier.name !== 'undefined' && identifier.name.indexOf('${') === 0;
+      });
+
+      if (foundVarRef.length > 0) {
+        colRefDeferral.resolve({ type: 'T' });
+      } else {
+        self.fetchFieldsForIdentifiers(self.parseResult.colRef.identifierChain, colRefCallback, function () {
+          colRefDeferral.resolve({ type: 'T' });
+        });
+      }
+    } else {
+      colRefDeferral.resolve();
+    }
+    return colRefDeferral;
+  };
+
+  /**
+   * Helper function to fetch columns/fields given an identifierChain, this also takes care of expanding arrays
+   * and maps to match the required format for the API.
+   *
+   * @param originalIdentifierChain
+   * @param callback
+   * @param errorCallback
+   */
+
+  Suggestions.prototype.fetchFieldsForIdentifiers = function (originalIdentifierChain, callback, errorCallback) {
+    var self = this;
+    var identifierChain = originalIdentifierChain.concat();
+
+    var fetchFieldsInternal =  function (table, database, identifierChain, callback, errorCallback, fetchedFields) {
+      if (!identifierChain) {
+        identifierChain = [];
+      }
+      if (identifierChain.length > 0) {
+        fetchedFields.push(identifierChain[0].name);
+        identifierChain = identifierChain.slice(1);
+      }
+
+      // Parser sometimes knows if it's a map or array.
+      if (identifierChain.length > 0 && (identifierChain[0].name === 'item' || identifierChain[0].name === 'value')) {
+        fetchedFields.push(identifierChain[0].name);
+        identifierChain = identifierChain.slice(1);
+      }
+
+      self.apiHelper.fetchFields({
+        sourceType: self.sourceType,
+        databaseName: database,
+        tableName: table,
+        fields: fetchedFields,
+        timeout: self.timeout,
+        successCallback: function (data) {
+          if (self.sourceType === 'hive'
+              && typeof data.extended_columns !== 'undefined'
+              && data.extended_columns.length === 1
+              && data.extended_columns.length
+              && /^map|array|struct/i.test(data.extended_columns[0].type)) {
+            identifierChain.unshift({ name: data.extended_columns[0].name })
+          }
+          if (identifierChain.length > 0) {
+            if (typeof identifierChain[0].name !== 'undefined' && /value|item|key/i.test(identifierChain[0].name)) {
+              fetchedFields.push(identifierChain[0].name);
+              identifierChain.shift();
+            } else {
+              if (data.type === 'array') {
+                fetchedFields.push('item')
+              }
+              if (data.type === 'map') {
+                fetchedFields.push('value')
+              }
+            }
+            fetchFieldsInternal(table, database, identifierChain, callback, errorCallback, fetchedFields)
+          } else {
+            callback(data);
+          }
+        },
+        silenceErrors: true,
+        errorCallback: errorCallback
+      });
+    };
+
+    // For Impala the first parts of the identifier chain could be either database or table, either:
+    // SELECT | FROM database.table -or- SELECT | FROM table.column
+
+    // For Hive it could be either:
+    // SELECT col.struct FROM db.tbl -or- SELECT col.struct FROM tbl
+    if (self.sourceType === 'impala' || self.sourceType === 'hive') {
+      if (identifierChain.length > 1) {
+        self.apiHelper.loadDatabases({
+          sourceType: self.sourceType,
+          timeout: self.timeout,
+          successCallback: function (data) {
+            var foundDb = data.filter(function (db) {
+              return db.toLowerCase() === identifierChain[0].name.toLowerCase();
+            });
+            var databaseName = foundDb.length > 0 ? identifierChain.shift().name : self.defaultDatabase;
+            var tableName = identifierChain.shift().name;
+            fetchFieldsInternal(tableName, databaseName, identifierChain, callback, errorCallback, []);
+          },
+          silenceErrors: true,
+          errorCallback: errorCallback
+        });
+      } else {
+        var databaseName = self.defaultDatabase;
+        var tableName = identifierChain.shift().name;
+        fetchFieldsInternal(tableName, databaseName, identifierChain, callback, errorCallback, []);
+      }
+    } else {
+      var databaseName = identifierChain.length > 1 ? identifierChain.shift().name : self.defaultDatabase;
+      var tableName = identifierChain.shift().name;
+      fetchFieldsInternal(tableName, databaseName, identifierChain, callback, errorCallback, []);
+    }
+  };
+
+  /**
+   * @param {Object} options
+   * @param {Snippet} options.snippet
+   * @param {Number} options.timeout
+   * @constructor
+   */
+  function SqlAutocompleter3(options) {
+    var self = this;
+    self.snippet = options.snippet;
+    self.timeout = options.timeout;
+  }
+
+  SqlAutocompleter3.prototype.autocomplete = function (beforeCursor, afterCursor, callback, editor) {
+    var self = this;
+    var sourceType = self.snippet.type();
+    var parseResult = sql.parseSql(beforeCursor, afterCursor, sourceType, false);
+
+    if (typeof hueDebug !== 'undefined' && hueDebug.showParseResult) {
+      console.log(parseResult);
+    }
+
+    if (typeof editor !== 'undefined' && editor !== null) {
+      editor.showSpinner();
+    }
+
+    self.suggestions = new Suggestions({
+      parseResult: parseResult,
+      sourceType: sourceType,
+      defaultDatabase: parseResult.useDatabase || self.snippet.database(),
+      timeout: self.timeout,
+      callback: function () {
+        editor.hideSpinner();
+        console.log(self);
+      }
+    });
+  };
+
+  return SqlAutocompleter3;
+})();

+ 14 - 1
desktop/core/src/desktop/templates/common_header_footer_components.mako

@@ -87,7 +87,20 @@ from metadata.conf import has_optimizer, OPTIMIZER
         errorLoadingTablePreview: '${ _('There was a problem loading the preview') }'
       },
       user: '${ user.username }'
-    }
+    };
+
+    AutocompleterGlobals = {
+      i18n: {
+        keywords: '${ _('Keywords') }',
+        columns: '${ _('Columns') }',
+        tables: '${ _('Tables') }',
+        databases: '${ _('Databases') }',
+        popular: '${ _('Popular') }',
+        meta: {
+          keyword: '${ _('keyword') }'
+        }
+      }
+    };
   </script>
 
   <!--[if lt IE 9]>

+ 1 - 1
desktop/libs/notebook/src/notebook/templates/editor_components.mako

@@ -64,7 +64,7 @@ from notebook.conf import ENABLE_QUERY_BUILDER, ENABLE_QUERY_SCHEDULING, ENABLE_
 <script src="${ static('desktop/js/sqlFunctions.js') }"></script>
 <script src="${ static('desktop/js/autocomplete/sql.js') }"></script>
 <script src="${ static('desktop/js/sqlAutocompleter.js') }"></script>
-<script src="${ static('desktop/js/sqlAutocompleter2.js') }"></script>
+<script src="${ static('desktop/js/sqlAutocompleter3.js') }"></script>
 <script src="${ static('desktop/js/hdfsAutocompleter.js') }"></script>
 <script src="${ static('desktop/js/autocompleter.js') }"></script>
 <script src="${ static('desktop/js/hue.json.js') }"></script>