Browse Source

HUE-7820 [assist] Use the new data catalog for the left and right assist trees

Johan Ahlen 7 years ago
parent
commit
5681886e7a

+ 79 - 186
desktop/core/src/desktop/static/desktop/js/assist/assistDbEntry.js

@@ -16,15 +16,7 @@
 
 var AssistDbEntry = (function () {
   /**
-   * @param {Object} definition
-   * @param {string} definition.type
-   * @param {string} definition.name
-   * @param {boolean} [definition.isColumn]
-   * @param {boolean} [definition.isTable]
-   * @param {boolean} [definition.isView]
-   * @param {boolean} [definition.isDatabase]
-   * @param {boolean} [definition.isMapValue]
-   * @param {boolean} [definition.isArray]
+   * @param {DataCatalogEntry} catalogEntry
    * @param {AssistDbEntry} parent
    * @param {AssistDbSource} assistDbSource
    * @param {Object} filter
@@ -36,9 +28,9 @@ var AssistDbEntry = (function () {
    * @param {Object} navigationSettings
    * @constructor
    */
-  function AssistDbEntry (definition, parent, assistDbSource, filter, i18n, navigationSettings) {
+  function AssistDbEntry (catalogEntry, parent, assistDbSource, filter, i18n, navigationSettings) {
     var self = this;
-    self.definition = definition;
+    self.catalogEntry = catalogEntry;
     self.parent = parent;
     self.assistDbSource = assistDbSource;
     self.filter = filter;
@@ -51,13 +43,7 @@ var AssistDbEntry = (function () {
     self.isSearchVisible = assistDbSource.isSearchVisible;
     self.activeSort = assistDbSource.activeSort;
 
-    self.expandable = typeof definition.type === "undefined" || /table|view|struct|array|map/i.test(definition.type);
-
-    self.metadata = DataCatalog.getEntry({
-      sourceType: self.sourceType,
-      path: self.getHierarchy(),
-      silenceErrors: self.navigationSettings.rightAssist
-    });
+    self.expandable = self.catalogEntry.hasPossibleChildren();
 
     self.filterColumnNames = ko.observable(false);
     self.highlight = ko.observable(false);
@@ -87,10 +73,10 @@ var AssistDbEntry = (function () {
 
     self.filteredEntries = ko.pureComputed(function () {
       var facets = self.filter.querySpec().facets;
-      var tableAndViewFilterMatch = !self.definition.isDatabase || (self.filter.showTables && self.filter.showTables() && self.filter.showViews && self.filter.showViews());
+      var tableAndViewFilterMatch = !self.catalogEntry.isDatabase() || (self.filter.showTables && self.filter.showTables() && self.filter.showViews && self.filter.showViews());
       var facetMatch = !facets || Object.keys(facets).length === 0 || !facets['type']; // So far only type facet is used for SQL
       // Only text match on tables/views or columns if flag is set
-      var textMatch = (!self.definition.isDatabase && !self.filterColumnNames()) || (!self.filter.querySpec().text || self.filter.querySpec().text.length === 0);
+      var textMatch = (!self.catalogEntry.isDatabase() && !self.filterColumnNames()) || (!self.filter.querySpec().text || self.filter.querySpec().text.length === 0);
 
       if (tableAndViewFilterMatch && facetMatch && textMatch) {
         return self.entries();
@@ -99,21 +85,21 @@ var AssistDbEntry = (function () {
       return self.entries().filter(function (entry) {
         var match = true;
         if (!tableAndViewFilterMatch) {
-          match = entry.definition.isTable && self.filter.showTables() || entry.definition.isView && self.filter.showViews();
+          match = entry.catalogEntry.isTable() && self.filter.showTables() || entry.catalogEntry.isView() && self.filter.showViews();
         }
 
         if (match && !facetMatch) {
-          if (entry.definition.isColumn || entry.definition.isComplex) {
-            match = (Object.keys(facets['type']).length == 2 && facets['type']['table'] && facets['type']['view'])
-              || (Object.keys(facets['type']).length == 1 && (facets['type']['table'] || facets['type']['view']))
-              || facets['type'][entry.definition.type];
-          } else if (entry.definition.isView || entry.definition.isTable) {
-            match = (!facets['type']['table'] && !facets['type']['view']) || (facets['type']['table'] && entry.definition.isTable) || (facets['type']['view'] && entry.definition.isView);
+          if (entry.catalogEntry.isField()) {
+            match = (Object.keys(facets['type']).length === 2 && facets['type']['table'] && facets['type']['view'])
+              || (Object.keys(facets['type']).length === 1 && (facets['type']['table'] || facets['type']['view']))
+              || facets['type'][entry.catalogEntry.getType()];
+          } else if (entry.catalogEntry.isTableOrView()) {
+            match = (!facets['type']['table'] && !facets['type']['view']) || (facets['type']['table'] && entry.catalogEntry.isTable()) || (facets['type']['view'] && entry.catalogEntry.isView());
           }
         }
 
         if (match && !textMatch) {
-          var nameLower = entry.definition.name.toLowerCase();
+          var nameLower = entry.catalogEntry.name.toLowerCase();
           match = self.filter.querySpec().text.every(function (text) {
             return nameLower.indexOf(text.toLowerCase()) !== -1
           });
@@ -127,8 +113,8 @@ var AssistDbEntry = (function () {
       var result = [];
       var partialLower = partial.toLowerCase();
       self.entries().forEach(function (entry) {
-        if (entry.definition.name.toLowerCase().indexOf(partialLower) === 0) {
-          result.push(nonPartial + partial + entry.definition.name.substring(partial.length))
+        if (entry.catalogEntry.name.toLowerCase().indexOf(partialLower) === 0) {
+          result.push(nonPartial + partial + entry.catalogEntry.name.substring(partial.length))
         }
       });
       return result;
@@ -138,20 +124,20 @@ var AssistDbEntry = (function () {
     self.columnName = null;
     self.type = null;
     self.databaseName = self.getHierarchy()[0];
-    if (self.definition.isTable || self.definition.isView) {
-      self.tableName = self.definition.name;
+    if (self.catalogEntry.isTableOrView()) {
+      self.tableName = self.catalogEntry.name;
       self.columnName = null;
-      self.type = self.definition.type;
-    } else if (self.definition.isColumn) {
-      self.tableName = parent.definition.name;
-      self.columnName = self.definition.name;
+      self.type = self.catalogEntry.getType();
+    } else if (self.catalogEntry.isColumn()) {
+      self.tableName = parent.catalogEntry.name;
+      self.columnName = self.catalogEntry.name;
     }
 
     self.editorText = ko.pureComputed(function () {
-      if (self.definition.isTable || self.definition.isView) {
+      if (self.catalogEntry.isTableOrView()) {
         return self.getTableName();
       }
-      if (self.definition.isColumn) {
+      if (self.catalogEntry.isColumn()) {
         return self.getColumnName() + ', ';
       }
       return self.getComplexName() + ', ';
@@ -164,20 +150,20 @@ var AssistDbEntry = (function () {
       entry = entry.parent;
     }
     if (entry) {
-      return SqlUtils.backTickIfNeeded(sourceType, entry.definition.name);
+      return SqlUtils.backTickIfNeeded(sourceType, entry.catalogEntry.name);
     }
   };
 
   AssistDbEntry.prototype.getDatabaseName = function () {
-    return findNameInHierarchy(this, function (entry) { return entry.definition.isDatabase });
+    return findNameInHierarchy(this, function (entry) { return entry.catalogEntry.isDatabase() });
   };
 
   AssistDbEntry.prototype.getTableName = function () {
-    return findNameInHierarchy(this, function (entry) { return entry.definition.isTable || entry.definition.isView });
+    return findNameInHierarchy(this, function (entry) { return entry.catalogEntry.isTableOrView() });
   };
 
   AssistDbEntry.prototype.getColumnName = function () {
-    return findNameInHierarchy(this, function (entry) { return entry.definition.isColumn });
+    return findNameInHierarchy(this, function (entry) { return entry.catalogEntry.isColumn() });
   };
 
   AssistDbEntry.prototype.getComplexName = function () {
@@ -185,15 +171,15 @@ var AssistDbEntry = (function () {
     var sourceType = entry.sourceType;
     var parts = [];
     while (entry != null) {
-      if (entry.definition.isTable || entry.definition.isView) {
+      if (entry.catalogEntry.isTableOrView()) {
         break;
       }
-      if (entry.definition.isArray || entry.definition.isMapValue) {
+      if (entry.catalogEntry.isArray() || entry.catalogEntry.isMapValue()) {
         if (sourceType === 'hive') {
           parts.push("[]");
         }
       } else {
-        parts.push(SqlUtils.backTickIfNeeded(sourceType, entry.definition.name));
+        parts.push(SqlUtils.backTickIfNeeded(sourceType, entry.catalogEntry.name));
         parts.push(".");
       }
       entry = entry.parent;
@@ -212,15 +198,15 @@ var AssistDbEntry = (function () {
     }
 
     var type;
-    if (self.definition.isColumn && self.sourceType === 'solr') {
+    if (self.catalogEntry.isColumn() && self.sourceType === 'solr') {
       type = 'collection';
-    } else if (self.definition.isColumn) {
+    } else if (self.catalogEntry.isColumn()) {
       type = 'column';
-    } else if (self.definition.isComplex) {
+    } else if (self.catalogEntry.isComplex()) {
       type = 'complex';
-    } else if (self.definition.isTable) {
+    } else if (self.catalogEntry.isTable()) {
       type = 'table';
-    } else if (self.definition.isView) {
+    } else if (self.catalogEntry.isView()) {
       type = 'view';
     } else {
       type = 'database';
@@ -267,7 +253,7 @@ var AssistDbEntry = (function () {
       var foundEntry;
       $.each(self.entries(), function (idx, entry) {
         entry.highlight(false);
-        if (entry.definition.name === path[0]) {
+        if (entry.catalogEntry.name === path[0]) {
           foundEntry = entry;
         }
       });
@@ -309,7 +295,7 @@ var AssistDbEntry = (function () {
     }
   };
 
-  AssistDbEntry.prototype.loadEntries = function(callback, silenceErrors) {
+  AssistDbEntry.prototype.loadEntries = function(callback) {
     var self = this;
     if (!self.expandable || self.loading()) {
       return;
@@ -324,105 +310,27 @@ var AssistDbEntry = (function () {
       self.loading(false);
       self.loaded = true;
 
-      var newEntries = [];
-      var index = 0;
-      if (typeof sourceMeta.tables_meta !== "undefined") {
-        newEntries = $.map(sourceMeta.tables_meta, function(table) {
-          table.index = index++;
-          table.title = table.name + (table.comment ? ' - ' + table.comment : '');
-          table.displayName = table.name;
-          table.isTable = /table/i.test(table.type);
-          table.isView = /view/i.test(table.type);
-          return self.createEntry(table);
-        });
-      } else if (typeof sourceMeta.extended_columns !== "undefined" && sourceMeta.extended_columns !== null) {
-        newEntries = $.map(sourceMeta.extended_columns, function (columnDef) {
-          var displayName = columnDef.name;
-          if (typeof columnDef.type !== "undefined" && columnDef.type !== null) {
-            displayName += ' (' + columnDef.type + ')'
-          }
-          var title = displayName;
-          if (typeof columnDef.comment !== "undefined" && columnDef.comment !== null) {
-            title += ' ' + columnDef.comment;
-          }
-          var shortType = null;
-          if (typeof columnDef.type !== "undefined" && columnDef.type !== null) {
-            shortType = columnDef.type.match(/^[^<]*/g)[0]; // everything before '<'
-          }
-          columnDef.index = index++;
-          columnDef.displayName = displayName;
-          columnDef.title = title;
-          columnDef.isColumn = true;
-          columnDef.type = shortType;
-          return self.createEntry(columnDef);
-        });
-      } else if (typeof sourceMeta.columns !== "undefined" && sourceMeta.columns !== null) {
-        newEntries = $.map(sourceMeta.columns, function(columnName) {
-          return self.createEntry({
-            name: columnName,
-            index: index++,
-            displayName: columnName,
-            title: columnName,
-            isColumn: true
-          });
-        });
-      } else if (sourceMeta.type === 'map') {
-        newEntries = [
-          self.createEntry({
-            name: "key",
-            index: index++,
-            displayName: "key (" + sourceMeta.key.type + ")",
-            title: "key (" + sourceMeta.key.type + ")",
-            type: sourceMeta.key.type,
-            isComplex: true
-          }),
-          self.createEntry({
-            name: "value",
-            index: index++,
-            displayName: "value (" + sourceMeta.value.type + ")",
-            title: "value (" + sourceMeta.value.type + ")",
-            isMapValue: true,
-            type: sourceMeta.value.type,
-            isComplex: true
-          })
-        ];
-      } else if (sourceMeta.type === 'struct') {
-        newEntries = $.map(sourceMeta.fields, function(field) {
-          return self.createEntry({
-            name: field.name,
-            index: index++,
-            displayName: field.name + " (" + field.type + ")",
-            title: field.name + " (" + field.type + ")",
-            type: field.type,
-            isComplex: true
-          });
+
+      self.catalogEntry.getChildren().done(function (catalogEntries) {
+        var newEntries = [];
+        catalogEntries.forEach(function (catalogEntry) {
+          newEntries.push(self.createEntry(catalogEntry));
         });
-      } else if (sourceMeta.type === 'array') {
-        newEntries = [
-          self.createEntry({
-            name: "item",
-            index: index++,
-            displayName: "item (" + sourceMeta.item.type + ")",
-            title: "item (" + sourceMeta.item.type + ")",
-            isArray: true,
-            type: sourceMeta.item.type,
-            isComplex: true
-          })
-        ];
-      }
+        if (sourceMeta.type === 'array' || sourceMeta.type === 'map') {
+          self.entries(newEntries);
+          self.entries()[0].open(true);
+        } else {
+          newEntries.sort(self.sortFunctions[self.assistDbSource.activeSort()]);
+          self.entries(newEntries);
+        }
 
-      if (sourceMeta.type === 'array' || sourceMeta.type === 'map') {
-        self.entries(newEntries);
-        self.entries()[0].open(true);
-      } else {
-        newEntries.sort(self.sortFunctions[self.assistDbSource.activeSort()]);
-        self.entries(newEntries);
-      }
+        loadEntriesDeferred.resolve(newEntries);
+        if (typeof callback === 'function') {
+          callback();
+        }
+      });
 
-      loadEntriesDeferred.resolve(newEntries);
-      if (typeof callback === 'function') {
-        callback();
-      }
+      self.catalogEntry.loadNavigatorMetaForChildren({ silenceErrors: self.navigationSettings.rightAssist });
     };
 
     var errorCallback = function () {
@@ -432,7 +340,7 @@ var AssistDbEntry = (function () {
       loadEntriesDeferred.resolve([]);
     };
 
-    if (!self.navigationSettings.rightAssist && HAS_OPTIMIZER && self.definition.isTable && self.assistDbSource.sourceType !== 'solr') {
+    if (!self.navigationSettings.rightAssist && HAS_OPTIMIZER && self.catalogEntry.isTable() && self.assistDbSource.sourceType !== 'solr') {
       self.assistDbSource.apiHelper.fetchNavOptTopColumns({
         sourceType: self.assistDbSource.sourceType,
         successCallback: function (data) {
@@ -444,8 +352,8 @@ var AssistDbEntry = (function () {
             $.when(loadEntriesDeferred).done(function () {
               if (!self.hasErrors()) {
                 self.entries().forEach(function (entry) {
-                  if (colIndex[entry.definition.name]) {
-                    entry.popularity(colIndex[entry.definition.name].columnCount);
+                  if (colIndex[entry.catalogEntry.name]) {
+                    entry.popularity(colIndex[entry.catalogEntry.name].columnCount);
                   }
                 });
 
@@ -457,47 +365,32 @@ var AssistDbEntry = (function () {
           }
         },
         silenceErrors: true,
-        tables: [{ identifierChain: [{ name: self.definition.name }] }],
-        defaultDatabase: self.parent.definition.name
+        tables: [{ identifierChain: [{ name: self.catalogEntry.name }] }],
+        defaultDatabase: self.parent.catalogEntry.name
       })
     }
 
-    self.metadata.getSourceMeta().done(successCallback).fail(errorCallback);
+    self.catalogEntry.getSourceMeta({ silenceErrors: self.navigationSettings.rightAssist }).done(successCallback).fail(errorCallback);
   };
 
   /**
-   * @param {Object} definition
-   * @param {string} definition.type
-   * @param {string} definition.name
-   * @param {boolean} [definition.isDatabase]
-   * @param {boolean} [definition.isColumn]
-   * @param {boolean} [definition.isTable]
-   * @param {boolean} [definition.isView]
-   * @param {boolean} [definition.isMapValue]
-   * @param {boolean} [definition.isArray]
+   * @param {DataCatalogEntry} catalogEntry
    */
-  AssistDbEntry.prototype.createEntry = function (definition) {
+  AssistDbEntry.prototype.createEntry = function (catalogEntry) {
     var self = this;
-    return new AssistDbEntry(definition, self, self.assistDbSource, self.filter, self.i18n, self.navigationSettings, self.sortFunctions)
+    return new AssistDbEntry(catalogEntry, self, self.assistDbSource, self.filter, self.i18n, self.navigationSettings)
   };
 
   AssistDbEntry.prototype.getHierarchy = function () {
     var self = this;
-    var parts = [];
-    var entry = self;
-    while (entry != null) {
-      parts.push(entry.definition.name);
-      entry = entry.parent;
-    }
-    parts.reverse();
-    return parts;
+    return self.catalogEntry.path.concat();
   };
 
   AssistDbEntry.prototype.dblClick = function () {
     var self = this;
-    if (self.definition.isTable || self.definition.isView) {
+    if (self.catalogEntry.isTableOrView()) {
       huePubSub.publish('editor.insert.table.at.cursor', { name: self.getTableName(), database: self.getDatabaseName() });
-    } else if (self.definition.isColumn) {
+    } else if (self.catalogEntry.isColumn()) {
       huePubSub.publish('editor.insert.column.at.cursor', { name: self.getColumnName(), table: self.getTableName(), database: self.getDatabaseName() });
     } else {
       huePubSub.publish('editor.insert.column.at.cursor', { name: self.getComplexName(), table: self.getTableName(), database: self.getDatabaseName() });
@@ -507,7 +400,7 @@ var AssistDbEntry = (function () {
   AssistDbEntry.prototype.explore = function (isSolr) {
     var self = this;
     if (isSolr) {
-      huePubSub.publish('open.link', '/hue/dashboard/browse/' + self.definition.name);
+      huePubSub.publish('open.link', '/hue/dashboard/browse/' + self.catalogEntry.name);
     }
     else {
       huePubSub.publish('open.link', '/hue/dashboard/browse/' + self.getDatabaseName() + '.' + self.getTableName() + '?engine=' + self.assistDbSource.sourceType);
@@ -517,10 +410,10 @@ var AssistDbEntry = (function () {
   AssistDbEntry.prototype.openInMetastore = function () {
     var self = this;
     var url;
-    if (self.definition.isDatabase) {
-      url = '/metastore/tables/' + self.definition.name;
-    } else if (self.definition.isTable || self.definition.isView) {
-      url = '/metastore/table/' + self.parent.definition.name + '/' + self.definition.name;
+    if (self.catalogEntry.isDatabase()) {
+      url = '/metastore/tables/' + self.catalogEntry.name;
+    } else if (self.catalogEntry.isTableOrView()) {
+      url = '/metastore/table/' + self.parent.catalogEntry.name + '/' + self.catalogEntry.name;
     } else {
       return;
     }
@@ -533,7 +426,8 @@ var AssistDbEntry = (function () {
   };
 
   AssistDbEntry.prototype.openInIndexer = function () {
-    var definitionName = this.definition.name;
+    var self = this;
+    var definitionName = self.catalogEntry.name;
     if (IS_NEW_INDEXER_ENABLED) {
       if (IS_HUE_4) {
         huePubSub.publish('open.link', '/indexer/indexes/' + definitionName);
@@ -565,7 +459,6 @@ var AssistDbEntry = (function () {
     }
   };
 
-
   AssistDbEntry.prototype.toggleOpen = function () {
     var self = this;
     self.open(!self.open());
@@ -573,15 +466,15 @@ var AssistDbEntry = (function () {
 
   AssistDbEntry.prototype.openItem = function () {
     var self = this;
-    if (self.definition.isTable || self.definition.isView) {
+    if (self.catalogEntry.isTableOrView()) {
       huePubSub.publish("assist.table.selected", {
         database: self.databaseName,
-        name: self.definition.name
+        name: self.catalogEntry.name
       })
-    } else if (self.definition.isDatabase) {
+    } else if (self.catalogEntry.isDatabase()) {
       huePubSub.publish("assist.database.selected", {
         source: self.assistDbSource.sourceType,
-        name: self.definition.name
+        name: self.catalogEntry.name
       })
     }
   };

+ 17 - 20
desktop/core/src/desktop/static/desktop/js/assist/assistDbSource.js

@@ -18,11 +18,11 @@ var AssistDbSource = (function () {
 
   var sortFunctions = {
     alpha: function (a, b) {
-      return a.definition.name.localeCompare(b.definition.name);
+      return a.catalogEntry.name.localeCompare(b.catalogEntry.name);
     },
     creation: function (a, b) {
-      if (a.definition.isColumn || a.definition.isComplex) {
-        return a.definition.index - b.definition.index;
+      if (a.catalogEntry.isField()) {
+        return a.catalogEntry.getIndex() - b.catalogEntry.getIndex();
       }
       return sortFunctions.alpha(a, b);
     },
@@ -99,7 +99,7 @@ var AssistDbSource = (function () {
       }
       var result = [];
       $.each(self.databases(), function (index, database) {
-        if (database.definition.name.toLowerCase().indexOf(self.filter.querySpec().query.toLowerCase()) > -1) {
+        if (database.catalogEntry.name.toLowerCase().indexOf(self.filter.querySpec().query.toLowerCase()) > -1) {
           result.push(database);
         }
       });
@@ -110,8 +110,8 @@ var AssistDbSource = (function () {
       var result = [];
       var partialLower = partial.toLowerCase();
       self.databases().forEach(function (db) {
-        if (db.definition.name.toLowerCase().indexOf(partialLower) === 0) {
-          result.push(nonPartial + partial + db.definition.name.substring(partial.length))
+        if (db.catalogEntry.name.toLowerCase().indexOf(partialLower) === 0) {
+          result.push(nonPartial + partial + db.catalogEntry.name.substring(partial.length))
         }
       });
       return result;
@@ -124,7 +124,7 @@ var AssistDbSource = (function () {
       if (HAS_OPTIMIZER && db && !db.popularityIndexSet && self.sourceType !== 'solr') {
         self.apiHelper.fetchNavOptTopTables({
           sourceType: self.sourceType,
-          database: db.definition.name,
+          database: db.catalogEntry.name,
           silenceErrors: true,
           timeout: AUTOCOMPLETE_TIMEOUT,
           successCallback: function (data) {
@@ -136,8 +136,8 @@ var AssistDbSource = (function () {
             });
             var applyPopularity = function () {
               db.entries().forEach(function (entry) {
-                if (popularityIndex[entry.definition.name]) {
-                  entry.popularity(popularityIndex[entry.definition.name]);
+                if (popularityIndex[entry.catalogEntry.name]) {
+                  entry.popularity(popularityIndex[entry.catalogEntry.name]);
                 }
               });
               if (self.activeSort() === 'popular') {
@@ -183,10 +183,10 @@ var AssistDbSource = (function () {
         if (!self.selectedDatabase().hasEntries() && !self.selectedDatabase().loading()) {
           self.selectedDatabase().loadEntries()
         }
-        self.apiHelper.setInTotalStorage('assist_' + self.sourceType, 'lastSelectedDb', self.selectedDatabase().definition.name)
+        self.apiHelper.setInTotalStorage('assist_' + self.sourceType, 'lastSelectedDb', self.selectedDatabase().catalogEntry.name)
         huePubSub.publish("assist.database.set", {
           source: self.sourceType,
-          name: self.selectedDatabase().definition.name
+          name: self.selectedDatabase().catalogEntry.name
         })
       }
     };
@@ -218,12 +218,9 @@ var AssistDbSource = (function () {
     var updateDatabases = function (names, lastSelectedDb) {
       dbIndex = {};
       var dbs = $.map(names, function(name) {
-        var database = new AssistDbEntry({
-          name: name,
-          displayName: name,
-          title: name,
-          isDatabase: true
-        }, null, self, nestedFilter, self.i18n, self.navigationSettings, sortFunctions);
+        var catalogEntry = DataCatalog.getEntry({ sourceType: self.sourceType, path: name });
+
+        var database = new AssistDbEntry(catalogEntry, null, self, nestedFilter, self.i18n, self.navigationSettings);
         dbIndex[name] = database;
         if (name === lastSelectedDb) {
           self.selectedDatabase(database);
@@ -240,7 +237,7 @@ var AssistDbSource = (function () {
     };
 
     self.setDatabase = function (databaseName) {
-      if (databaseName && self.selectedDatabase() && databaseName === self.selectedDatabase().definition.name) {
+      if (databaseName && self.selectedDatabase() && databaseName === self.selectedDatabase().catalogEntry.name) {
         return;
       }
       if (databaseName && dbIndex[databaseName]) {
@@ -263,7 +260,7 @@ var AssistDbSource = (function () {
         return;
       }
       self.loading(true);
-      var lastSelectedDb = self.selectedDatabase() ? self.selectedDatabase().definition.name : null;
+      var lastSelectedDb = self.selectedDatabase() ? self.selectedDatabase().catalogEntry.name : null;
       self.selectedDatabase(null);
       self.databases([]);
       self.apiHelper.loadDatabases({
@@ -294,7 +291,7 @@ var AssistDbSource = (function () {
       if (self.invalidateOnRefresh() !== 'cache') {
         huePubSub.publish('assist.invalidate.impala', {
           flush: self.invalidateOnRefresh() === 'invalidateAndFlush',
-          database: self.selectedDatabase() ? self.selectedDatabase().definition.name : null
+          database: self.selectedDatabase() ? self.selectedDatabase().catalogEntry.name : null
         });
       }
 

+ 281 - 68
desktop/core/src/desktop/static/desktop/js/dataCatalog.js

@@ -17,99 +17,137 @@
 var DataCatalog = (function () {
 
 
+  /**
+   * @param {String} sourceType
+   * @constructor
+   */
   function DataCatalog(sourceType) {
     var self = this;
     self.sourceType = sourceType;
-
+    self.entries = {};
   }
 
-  DataCatalog.prototype.getEntry = function (options) {
+  /**
+   * @param {string|string[]} path
+   * @return {DataCatalogEntry}
+   */
+  DataCatalog.prototype.getEntry = function (path) {
     var self = this;
-    if (typeof options === 'string') {
-      return new DataCatalogEntry(self, { path: options ? options.split('.') : [] })
-    }
-    return new DataCatalogEntry(self, options);
+    var identifier = typeof path === 'string' ? path : (typeof path === 'string' ? path : path.join('.'));
+    return self.entries[identifier] || (self.entries[identifier] = new DataCatalogEntry(self, path));
   };
 
-  var reloadSourceMeta = function (dataCatalogEntry, refreshCache) {
+  /**
+   * @param {DataCatalogEntry} dataCatalogEntry
+   * @param {Object} [apiOptions]
+   * @param {boolean} [apiOptions.silenceErrors]
+   * @param {boolean} [apiOptions.cachedOnly]
+   * @param {boolean} [apiOptions.refreshCache]
+   * @return {Promise}
+   */
+  var reloadSourceMeta = function (dataCatalogEntry, apiOptions) {
     var deferred = $.Deferred();
     ApiHelper.getInstance().fetchSourceMetadata({
       sourceType: dataCatalogEntry.dataCatalog.sourceType,
       path: dataCatalogEntry.path,
-      silenceErrors: dataCatalogEntry.silenceErrors,
-      cachedOnly: dataCatalogEntry.cachedOnly,
-      refreshCache: refreshCache
+      silenceErrors: apiOptions && apiOptions.silenceErrors,
+      cachedOnly: apiOptions && apiOptions.cachedOnly,
+      refreshCache: apiOptions && apiOptions.refreshCache
     }).done(function (data) {
+      dataCatalogEntry.sourceMeta = data;
       deferred.resolve(data);
     }).fail(deferred.reject);
-    dataCatalogEntry.lastSourcePromise = deferred.promise();
-    return dataCatalogEntry.lastSourcePromise;
+    dataCatalogEntry.lastSourceMetaPromise = deferred.promise();
+    return dataCatalogEntry.lastSourceMetaPromise;
   };
 
-  var reloadNavigatorMeta = function (dataCatalogEntry, refreshCache) {
+  /**
+   * @param {DataCatalogEntry} dataCatalogEntry
+   * @param {Object} [apiOptions]
+   * @param {boolean} [apiOptions.silenceErrors]
+   * @param {boolean} [apiOptions.cachedOnly]
+   * @param {boolean} [apiOptions.refreshCache]
+   * @return {Promise}
+   */
+  var reloadNavigatorMeta = function (dataCatalogEntry, apiOptions) {
     var deferred = $.Deferred();
     if (HAS_NAVIGATOR) {
       ApiHelper.getInstance().fetchNavigatorMetadata({
         path: dataCatalogEntry.path,
-        silenceErrors: dataCatalogEntry.silenceErrors,
-        refreshCache: refreshCache
+        silenceErrors: apiOptions && apiOptions.silenceErrors,
+        cachedOnly: apiOptions && apiOptions.cachedOnly,
+        refreshCache: apiOptions && apiOptions.refreshCache
       }).done(function (data) {
+        dataCatalogEntry.navigatorMeta = data;
         deferred.resolve(data);
       }).fail(deferred.reject);
     } else {
       deferred.reject();
     }
-    dataCatalogEntry.lastNavigatorPromise = deferred.promise();
-    return dataCatalogEntry.lastNavigatorPromise;
+    dataCatalogEntry.lastNavigatorMetaPromise = deferred.promise();
+    return dataCatalogEntry.lastNavigatorMetaPromise;
   };
 
-  function DataCatalogEntry(dataCatalog, options) {
+  /**
+   * @param {DataCatalog} dataCatalog
+   * @param {string|string[]} path
+   * @constructor
+   */
+  function DataCatalogEntry(dataCatalog, path) {
     var self = this;
 
     self.dataCatalog = dataCatalog;
-    self.path = typeof options.path === 'string' && options.path ? options.path.split('.') : options.path || [];
+    self.path = typeof path === 'string' && path ? path.split('.') : path || [];
+    self.name = self.path.length ? self.path[self.path.length - 1] : dataCatalog.sourceType;
 
-    self.partialSourceMeta = options.partialSourceMeta;
-    self.lastSourcePromise = undefined;
-    self.lastNavigatorPromise = undefined;
+    self.definition = undefined;
+    self.lastSourceMetaPromise = undefined;
+    self.sourceMeta = undefined;
+    self.lastNavigatorMetaPromise = undefined;
+    self.navigatorMeta = undefined;
 
     self.children = undefined;
-    self.lastNavigatorChildrenPromise = undefined;
-
-    self.silenceErrors = options.silenceErrors;
-    self.cachedOnly = options.cachedOnly;
   }
 
-  DataCatalogEntry.prototype.getChildren = function () {
+  /**
+   * @param {Object} [apiOptions]
+   * @param {boolean} [apiOptions.silenceErrors]
+   * @param {boolean} [apiOptions.cachedOnly]
+   * @param {boolean} [apiOptions.refreshCache]
+   * @return {Promise}
+   */
+  DataCatalogEntry.prototype.getChildren = function (apiOptions) {
     var self = this;
     var deferred = $.Deferred();
     if (self.children) {
       deferred.resolve(self.children);
     } else {
-      self.getSourceMeta().done(function (sourceMeta) {
+      self.getSourceMeta(apiOptions).done(function (sourceMeta) {
         if (self.children) {
           deferred.resolve(self.children);
         } else {
           self.children = [];
+          var index = 0;
           var entities = sourceMeta.databases || sourceMeta.tables_meta || sourceMeta.extended_columns || sourceMeta.fields;
           if (entities) {
             entities.forEach(function (entity) {
-              self.children.push(new DataCatalogEntry(self.dataCatalog, {
-                path: self.path.concat(entity.name || entity),
-                silenceErrors: self.silenceErrors,
-                cachedOnly: self.cachedOnly,
-                partialSourceMeta: typeof entity === 'object' ? entity : undefined
-              }))
+              var catalogEntry = self.dataCatalog.getEntry(self.path.concat(entity.name || entity));
+              if (!catalogEntry.definition && typeof entity === 'object') {
+                catalogEntry.definition = entity;
+                catalogEntry.definition.index = index++;
+              }
+              self.children.push(catalogEntry);
             });
           } else {
             (sourceMeta.type === 'map' ? ['key', 'value'] : ['item']).forEach(function (path) {
               if (sourceMeta[path]) {
-                self.children.push(new DataCatalogEntry(self.dataCatalog, {
-                  path: self.path.concat(path),
-                  silenceErrors: self.silenceErrors,
-                  cachedOnly: self.cachedOnly,
-                  partialSourceMeta: sourceMeta[path]
-                }));
+                var catalogEntry = self.dataCatalog.getEntry(self.path.concat(path));
+                if (!catalogEntry.definition) {
+                  catalogEntry.definition = sourceMeta[path];
+                  catalogEntry.definition.index = index++;
+                  catalogEntry.definition.isMapValue = path === 'value'
+                }
+                self.children.push(catalogEntry);
               }
             })
           }
@@ -120,8 +158,14 @@ var DataCatalog = (function () {
     return deferred.promise();
   };
 
-  DataCatalogEntry.prototype.loadNavigatorMetaForChildren = function () {
+  /**
+   * @param {Object} [apiOptions]
+   * @param {boolean} [apiOptions.silenceErrors]
+   * @return {Promise}
+   */
+  DataCatalogEntry.prototype.loadNavigatorMetaForChildren = function (apiOptions) {
     var self = this;
+    var deferred = $.Deferred();
     self.getChildren().done(function (children) {
       var query;
 
@@ -135,7 +179,8 @@ var DataCatalog = (function () {
       ApiHelper.getInstance().searchEntities({
         query: query,
         rawQuery: true,
-        limit: children.length
+        limit: children.length,
+        silenceErrors: apiOptions && apiOptions.silenceErrors
       }).done(function (result) {
         if (result && result.entities && result.entities.length > 0) {
           var entityIndex = {};
@@ -145,32 +190,50 @@ var DataCatalog = (function () {
           children.forEach(function (child) {
             var name = child.path[child.path.length - 1];
             if (entityIndex[name]) {
-              child.lastNavigatorPromise = $.Deferred().resolve(entityIndex[name]).promise();
+              child.navigatorMeta = entityIndex[name];
+              child.lastNavigatorMetaPromise = $.Deferred().resolve(child.navigatorMeta).promise();
             }
           });
+          deferred.resolve(children);
         }
-      })
-    })
+      }).fail(deferred.reject);
+    });
+    return deferred.promise();
   };
 
-  DataCatalogEntry.prototype.getComment = function () {
+  /**
+   * @param {Object} [apiOptions]
+   * @param {boolean} [apiOptions.silenceErrors]
+   * @param {boolean} [apiOptions.cachedOnly]
+   * @param {boolean} [apiOptions.refreshCache]
+   * @return {Promise}
+   */
+  DataCatalogEntry.prototype.getComment = function (apiOptions) {
     var self = this;
     var deferred = $.Deferred();
 
     var resolveWithSourceMeta = function () {
-      self.getSourceMeta().done(function (sourceMeta) {
-        deferred.resolve(sourceMeta && sourceMeta.comment || '');
-      }).fail(deferred.reject);
+      if (self.sourceMeta) {
+        deferred.resolve(self.sourceMeta && self.sourceMeta.comment || '');
+      } else {
+        self.getSourceMeta(apiOptions).done(function (sourceMeta) {
+          deferred.resolve(sourceMeta && sourceMeta.comment || '');
+        }).fail(deferred.reject);
+      }
     };
 
     if (HAS_NAVIGATOR) {
-      self.getNavigatorMeta().done(function (navigatorMeta) {
-        if (navigatorMeta && navigatorMeta.entity) {
-          deferred.resolve(navigatorMeta.entity.description || navigatorMeta.entity.originalDescription || '');
-        } else {
-          resolveWithSourceMeta();
-        }
-      }).fail(resolveWithSourceMeta)
+      if (self.navigatorMeta && self.navigatorMeta.entity) {
+        deferred.resolve(self.navigatorMeta.entity.description || self.navigatorMeta.entity.originalDescription || '');
+      } else {
+        self.getNavigatorMeta(apiOptions).done(function (navigatorMeta) {
+          if (navigatorMeta && navigatorMeta.entity) {
+            deferred.resolve(navigatorMeta.entity.description || navigatorMeta.entity.originalDescription || '');
+          } else {
+            resolveWithSourceMeta();
+          }
+        }).fail(resolveWithSourceMeta)
+      }
     } else {
       resolveWithSourceMeta();
     }
@@ -178,12 +241,20 @@ var DataCatalog = (function () {
     return deferred.promise();
   };
 
-  DataCatalogEntry.prototype.setComment = function (comment) {
+  /**
+   * @param {string} comment
+   * @param {Object} [apiOptions]
+   * @param {boolean} [apiOptions.silenceErrors]
+   * @param {boolean} [apiOptions.cachedOnly]
+   * @param {boolean} [apiOptions.refreshCache]
+   * @return {Promise}
+   */
+  DataCatalogEntry.prototype.setComment = function (comment, apiOptions) {
     var self = this;
     var deferred = $.Deferred();
 
     if (HAS_NAVIGATOR) {
-      self.getNavigatorMeta().done(function (navigatorMeta) {
+      self.getNavigatorMeta(apiOptions).done(function (navigatorMeta) {
         if (navigatorMeta && navigatorMeta.entity) {
           ApiHelper.getInstance().updateNavigatorMetadata({
             identity: navigatorMeta.entity.identity,
@@ -191,8 +262,12 @@ var DataCatalog = (function () {
               description: comment
             }
           }).done(function () {
-            reloadNavigatorMeta(self, true);
-            self.getComment().done(deferred.resolve);
+            reloadNavigatorMeta(self, {
+              silenceErrors: apiOptions && apiOptions.silenceErrors,
+              refreshCache: true
+            }).done(function() {
+              self.getComment(apiOptions).done(deferred.resolve);
+            });
           }).fail(deferred.reject);
         }
       }).fail(deferred.reject);
@@ -204,35 +279,173 @@ var DataCatalog = (function () {
           comment: comment
         }
       }).done(function () {
-        reloadSourceMeta(self, true);
-        self.getComment().done(deferred.resolve);
+        reloadSourceMeta(self, {
+          silenceErrors: apiOptions && apiOptions.silenceErrors,
+          refreshCache: true
+        }).done(function () {
+          self.getComment(apiOptions).done(deferred.resolve);
+        });
       }).fail(deferred.reject);
     }
 
     return deferred.promise();
   };
 
-  DataCatalogEntry.prototype.getSourceMeta = function () {
+  DataCatalogEntry.prototype.hasPossibleChildren = function () {
+    var self = this;
+    return (!self.definition && !self.sourceMeta) ||
+      (self.sourceMeta && /^(?:table|view|struct|array|map)/i.test(self.sourceMeta.type)) ||
+      (self.definition && /^(?:table|view|struct|array|map)/i.test(self.definition.type));
+  };
+
+  DataCatalogEntry.prototype.getIndex = function () {
+    var self = this;
+    return self.definition && self.definition.index ? self.definition.index : 0;
+  };
+
+  DataCatalogEntry.prototype.isDatabase = function () {
+    var self = this;
+    return self.path.length === 1;
+  };
+
+  DataCatalogEntry.prototype.isTableOrView = function () {
+    var self = this;
+    return self.path.length === 2;
+  };
+
+
+  DataCatalogEntry.prototype.getTitle = function () {
+    var self = this;
+    var title = self.path.join('.');
+    if (self.isField()) {
+      var type = self.getType();
+      if (type) {
+        if (~type.indexOf('<')) {
+          type = type.substring(0, type.indexOf('<'));
+        }
+        title += ' (' + type + ')';
+      }
+    }
+    return title;
+  };
+
+  DataCatalogEntry.prototype.getDisplayName = function () {
+    var self = this;
+    var displayName = self.name;
+    if (self.isField()) {
+      var type = self.getType();
+      if (type) {
+        if (~type.indexOf('<')) {
+          type = type.substring(0, type.indexOf('<'));
+        }
+        displayName += ' (' + type + ')';
+      }
+    }
+    return displayName;
+  };
+
+  DataCatalogEntry.prototype.isPrimaryKey = function () {
+    var self = this;
+    return self.isColumn() && self.definition && /true/i.test(self.definition.primary_key);
+  };
+
+  DataCatalogEntry.prototype.isTable = function () {
+    var self = this;
+    if (self.path.length === 2) {
+      if (self.sourceMeta) {
+        return !self.sourceMeta.is_view;
+      }
+      if (self.definition && self.definition.type) {
+        return self.definition.type.toLowerCase() === 'table';
+      }
+      return true;
+    }
+    return false;
+  };
+
+  DataCatalogEntry.prototype.isView = function () {
+    var self = this;
+    return self.path.length === 2 &&
+      ((self.sourceMeta && self.sourceMeta.is_view) ||
+      (self.definition && self.definition.type && self.definition.type.toLowerCase() === 'view'));
+  };
+
+  DataCatalogEntry.prototype.isColumn = function () {
+    var self = this;
+    return self.path.length === 3;
+  };
+
+  DataCatalogEntry.prototype.isComplex = function () {
+    var self = this;
+    return self.path.length > 2 && (
+      (self.sourceMeta && /^(?:struct|array|map)/i.test(self.sourceMeta.type)) ||
+      (self.definition && /^(?:struct|array|map)/i.test(self.definition.type)));
+  };
+
+  DataCatalogEntry.prototype.isField = function () {
+    var self = this;
+    return self.path.length > 2;
+  };
+
+  DataCatalogEntry.prototype.isArray = function () {
+    var self = this;
+    return (self.definition && self.definition.type && self.definition.type.toLowerCase() === 'array') ||
+      (self.sourceMeta && self.sourceMeta.type && self.sourceMeta.type.toLowerCase() === 'array');
+  };
+
+  DataCatalogEntry.prototype.isMapValue = function () {
+    var self = this;
+    return self.definition && self.definition.isMapValue;
+  };
+
+  DataCatalogEntry.prototype.getType = function () {
+    var self = this;
+    if (self.sourceMeta) {
+      return self.sourceMeta.type;
+    }
+    if (self.definition) {
+      return self.definition.type;
+    }
+  };
+
+  /**
+   * @param {Object} [apiOptions]
+   * @param {boolean} [apiOptions.silenceErrors]
+   * @param {boolean} [apiOptions.cachedOnly]
+   * @param {boolean} [apiOptions.refreshCache]
+   * @return {Promise}
+   */
+  DataCatalogEntry.prototype.getSourceMeta = function (apiOptions) {
     var self = this;
-    return self.lastSourcePromise || reloadSourceMeta(self)
+    return self.lastSourceMetaPromise || reloadSourceMeta(self, apiOptions);
   };
 
-  DataCatalogEntry.prototype.getNavigatorMeta = function () {
+  /**
+   * @param {Object} [apiOptions]
+   * @param {boolean} [apiOptions.silenceErrors]
+   * @param {boolean} [apiOptions.cachedOnly]
+   * @param {boolean} [apiOptions.refreshCache]
+   * @return {Promise}
+   */
+  DataCatalogEntry.prototype.getNavigatorMeta = function (apiOptions) {
     var self = this;
-    return self.lastNavigatorPromise || reloadNavigatorMeta(self)
+    return self.lastNavigatorMetaPromise || reloadNavigatorMeta(self, apiOptions)
   };
 
 
   var instances = {};
 
+  /**
+   * @param {string} sourceType
+   * @return {DataCatalog}
+   */
   var getCatalog = function (sourceType) {
     return instances[sourceType] || (instances[sourceType] = new DataCatalog(sourceType));
   };
 
   return {
-    getCatalog: getCatalog,
     getEntry: function (options) {
-      return getCatalog(options.sourceType).getEntry(options);
+      return getCatalog(options.sourceType).getEntry(options.path);
     }
   };
 })();

+ 48 - 48
desktop/core/src/desktop/templates/assist.mako

@@ -87,7 +87,7 @@ from desktop.views import _ko
     <!-- ko if: typeof definition !== 'undefined' -->
       <!-- ko if: sourceType === 'solr' -->
         <li><a href="javascript:void(0);" data-bind="click: function (data) { showContextPopover(data, { target: $parentContext.$contextSourceElement }, { left: 4, top: 2 }); }"><i class="fa fa-fw fa-info"></i> ${ _('Show details') }</a></li>
-        <!-- ko if: definition.isView || definition.isTable -->
+        <!-- ko if: catalogEntry.isTableOrView() -->
         <li><a href="javascript:void(0);" data-bind="click: openInIndexer"><i class="fa fa-fw fa-table"></i> ${ _('Open in Browser') }</a></li>
         <li>
           <a href="javascript: void(0);" data-bind="click: function() { explore(true); }">
@@ -98,7 +98,7 @@ from desktop.views import _ko
       <!-- /ko -->
       <!-- ko ifnot: sourceType === 'solr' -->
         <li><a href="javascript:void(0);" data-bind="click: function (data) { showContextPopover(data, { target: $parentContext.$contextSourceElement }, { left: 4, top: 2 }); }"><i class="fa fa-fw fa-info"></i> ${ _('Show details') }</a></li>
-        <!-- ko if: !definition.isDatabase && $currentApp() === 'editor' -->
+        <!-- ko if: !catalogEntry.isDatabase() && $currentApp() === 'editor' -->
         <li><a href="javascript:void(0);" data-bind="click: dblClick"><i class="fa fa-fw fa-paste"></i> ${ _('Insert at cursor') }</a></li>
         <!-- /ko -->
         % if not IS_EMBEDDED.get():
@@ -106,7 +106,7 @@ from desktop.views import _ko
         <li><a href="javascript:void(0);" data-bind="click: openInMetastore"><i class="fa fa-fw fa-table"></i> ${ _('Open in Browser') }</a></li>
         <!-- /ko -->
         % endif
-        <!-- ko if: definition.isView || definition.isTable -->
+        <!-- ko if: catalogEntry.isTableOrView() -->
         <li>
           <a href="javascript:void(0);" data-bind="click: function() { huePubSub.publish('query.and.watch', {'url': '/notebook/browse/' + databaseName + '/' + tableName + '/', sourceType: sourceType}); }">
             <i class="fa fa-fw fa-code"></i> ${ _('Open in Editor') }
@@ -121,7 +121,7 @@ from desktop.views import _ko
         % endif
         <!-- /ko -->
         %if ENABLE_QUERY_BUILDER.get():
-        <!-- ko if: definition.isColumn && $currentApp() === 'editor' -->
+        <!-- ko if: catalogEntry.isColumn() && $currentApp() === 'editor' -->
         <li class="divider"></li>
         <!-- ko template: { name: 'query-builder-context-items' } --><!-- /ko -->
         <!-- /ko -->
@@ -203,7 +203,7 @@ from desktop.views import _ko
   <script type="text/html" id="assist-database-entry">
     <li class="assist-table" data-bind="appAwareTemplateContextMenu: { template: 'sql-context-items', scrollContainer: '.assist-db-scrollable' }, visibleOnHover: { selector: '.database-actions' }">
       <!-- ko template: { name: 'assist-database-actions' } --><!-- /ko -->
-      <a class="assist-table-link" href="javascript: void(0);" data-bind="click: function () { $parent.selectedDatabase($data); $parent.selectedDatabaseChanged(); }, attr: {'title': definition.title }, draggableText: { text: editorText,  meta: {'type': 'sql', 'database': databaseName} }"><i class="fa fa-fw fa-database muted valign-middle"></i> <span class="highlightable" data-bind="text: definition.name, css: { 'highlight': highlight() }"></span></a>
+      <a class="assist-table-link" href="javascript: void(0);" data-bind="click: function () { $parent.selectedDatabase($data); $parent.selectedDatabaseChanged(); }, attr: {'title': catalogEntry.getTitle() }, draggableText: { text: editorText,  meta: {'type': 'sql', 'database': databaseName} }"><i class="fa fa-fw fa-database muted valign-middle"></i> <span class="highlightable" data-bind="text: catalogEntry.name, css: { 'highlight': highlight() }"></span></a>
     </li>
   </script>
 
@@ -213,9 +213,9 @@ from desktop.views import _ko
         <a class="inactive-action" href="javascript:void(0)" data-bind="visible: navigationSettings.showStats, click: showContextPopover, css: { 'blue': statsVisible }"><i class="fa fa-fw fa-info" title="${_('Show details')}"></i></a>
         <a class="inactive-action" href="javascript:void(0)" data-bind="visible: navigationSettings.openItem, click: openItem"><i class="fa fa-long-arrow-right" title="${_('Open')}"></i></a>
       </div>
-      <a class="assist-entry assist-table-link" href="javascript:void(0)" data-bind="click: toggleOpen, attr: {'title': definition.title }, draggableText: { text: editorText,  meta: {'type': 'sql', 'isView': definition.isView, 'table': tableName, 'database': databaseName} }">
-        <i class="fa fa-fw fa-table muted valign-middle" data-bind="css: { 'fa-eye': definition.isView && !navigationSettings.rightAssist, 'fa-table': definition.isTable && sourceType !== 'solr' && !navigationSettings.rightAssist, 'fa-search': sourceType === 'solr' }"></i>
-        <span class="highlightable" data-bind="text: definition.displayName, css: { 'highlight': highlight }"></span> <!-- ko if: assistDbSource.activeSort() === 'popular' && popularity() > 0 --><i title="${ _('Popular') }" class="fa fa-star-o top-star"></i> <!-- /ko -->
+      <a class="assist-entry assist-table-link" href="javascript:void(0)" data-bind="click: toggleOpen, attr: {'title': catalogEntry.getTitle() }, draggableText: { text: editorText,  meta: {'type': 'sql', 'isView': catalogEntry.isView(), 'table': tableName, 'database': databaseName} }">
+        <i class="fa fa-fw fa-table muted valign-middle" data-bind="css: { 'fa-eye': catalogEntry.isView() && !navigationSettings.rightAssist, 'fa-table': catalogEntry.isTable() && sourceType !== 'solr' && !navigationSettings.rightAssist, 'fa-search': sourceType === 'solr' }"></i>
+        <span class="highlightable" data-bind="text: catalogEntry.getDisplayName(), css: { 'highlight': highlight }"></span> <!-- ko if: assistDbSource.activeSort() === 'popular' && popularity() > 0 --><i title="${ _('Popular') }" class="fa fa-star-o top-star"></i> <!-- /ko -->
       </a>
       <div class="center assist-spinner" data-bind="visible: loading() && open()"><i class="fa fa-spinner fa-spin"></i></div>
       <!-- ko template: { if: open, name: 'assist-db-entries'  } --><!-- /ko -->
@@ -223,18 +223,18 @@ from desktop.views import _ko
   </script>
 
   <script type="text/html" id="assist-column-entry">
-    <li data-bind="appAwareTemplateContextMenu: { template: 'sql-context-items', scrollContainer: '.assist-db-scrollable' }, visible: ! hasErrors(), visibleOnHover: { childrenOnly: true, override: statsVisible, selector: definition.isView ? '.table-actions' : '.column-actions' }, css: { 'assist-table': definition.isView, 'assist-column': definition.isColumn || definition.isComplex }">
+    <li data-bind="appAwareTemplateContextMenu: { template: 'sql-context-items', scrollContainer: '.assist-db-scrollable' }, visible: ! hasErrors(), visibleOnHover: { childrenOnly: true, override: statsVisible, selector: catalogEntry.isView() ? '.table-actions' : '.column-actions' }, css: { 'assist-table': catalogEntry.isView(), 'assist-column': catalogEntry.isField() }">
       <div class="assist-actions column-actions" data-bind="css: { 'assist-actions-left': navigationSettings.rightAssist }" style="opacity: 0">
         <a class="inactive-action" href="javascript:void(0)" data-bind="visible: navigationSettings.showStats, click: showContextPopover, css: { 'blue': statsVisible }"><i class="fa fa-fw fa-info" title="${_('Show details')}"></i></a>
       </div>
       <!-- ko if: expandable -->
-      <a class="assist-entry assist-field-link" href="javascript:void(0)" data-bind="click: toggleOpen, attr: {'title': definition.title }, css: { 'assist-entry-left-action': navigationSettings.rightAssist }">
-        <span class="highlightable" data-bind="css: { 'highlight': highlight}, attr: {'column': columnName, 'table': tableName, 'database': databaseName }, text: definition.displayName, draggableText: { text: editorText, meta: {'type': 'sql', 'column': columnName, 'table': tableName, 'database': databaseName } }"></span><!-- ko if: definition.primary_key === 'true' --> <i class="fa fa-key"></i><!-- /ko -->
+      <a class="assist-entry assist-field-link" href="javascript:void(0)" data-bind="click: toggleOpen, attr: {'title': catalogEntry.getTitle() }, css: { 'assist-entry-left-action': navigationSettings.rightAssist }">
+        <span class="highlightable" data-bind="css: { 'highlight': highlight}, attr: {'column': columnName, 'table': tableName, 'database': databaseName }, text: catalogEntry.getDisplayName(), draggableText: { text: editorText, meta: {'type': 'sql', 'column': columnName, 'table': tableName, 'database': databaseName } }"></span><!-- ko if: catalogEntry.isPrimaryKey() --> <i class="fa fa-key"></i><!-- /ko -->
       </a>
       <!-- /ko -->
       <!-- ko ifnot: expandable -->
-      <div class="assist-entry assist-field-link default-cursor" href="javascript:void(0)" data-bind="event: { dblclick: dblClick }, attr: {'title': definition.title }, css: { 'assist-entry-left-action': navigationSettings.rightAssist }">
-        <span class="highlightable" data-bind="css: { 'highlight': highlight}, attr: {'column': columnName, 'table': tableName, 'database': databaseName}, text: definition.displayName, draggableText: { text: editorText, meta: {'type': 'sql', 'column': columnName, 'table': tableName, 'database': databaseName} }"></span><!-- ko if: definition.primary_key === 'true'  --> <i class="fa fa-key"></i><!-- /ko --><!-- ko if: assistDbSource.activeSort() === 'popular' && popularity() > 0 --> <i title="${ _('Popular') }" class="fa fa-star-o top-star"></i> <!-- /ko -->
+      <div class="assist-entry assist-field-link default-cursor" href="javascript:void(0)" data-bind="event: { dblclick: dblClick }, attr: {'title': catalogEntry.getTitle() }, css: { 'assist-entry-left-action': navigationSettings.rightAssist }">
+        <span class="highlightable" data-bind="css: { 'highlight': highlight}, attr: {'column': columnName, 'table': tableName, 'database': databaseName}, text: catalogEntry.getDisplayName(), draggableText: { text: editorText, meta: {'type': 'sql', 'column': columnName, 'table': tableName, 'database': databaseName} }"></span><!-- ko if: catalogEntry.isPrimaryKey()  --> <i class="fa fa-key"></i><!-- /ko --><!-- ko if: assistDbSource.activeSort() === 'popular' && popularity() > 0 --> <i title="${ _('Popular') }" class="fa fa-star-o top-star"></i> <!-- /ko -->
       </div>
       <!-- /ko -->
       <div class="center assist-spinner" data-bind="visible: loading"><i class="fa fa-spinner fa-spin"></i></div>
@@ -243,20 +243,20 @@ from desktop.views import _ko
   </script>
 
   <script type="text/html" id="assist-column-entry-assistant">
-    <li data-bind="appAwareTemplateContextMenu: { template: 'sql-context-items', scrollContainer: '.assist-db-scrollable' }, visible: ! hasErrors(), visibleOnHover: { childrenOnly: true, override: statsVisible, selector: definition.isView ? '.table-actions' : '.column-actions' }, css: { 'assist-table': definition.isView, 'assist-column': definition.isColumn || definition.isComplex }">
+    <li data-bind="appAwareTemplateContextMenu: { template: 'sql-context-items', scrollContainer: '.assist-db-scrollable' }, visible: ! hasErrors(), visibleOnHover: { childrenOnly: true, override: statsVisible, selector: catalogEntry.isView() ? '.table-actions' : '.column-actions' }, css: { 'assist-table': catalogEntry.isView(), 'assist-column': catalogEntry.isField() }">
       <div class="assist-actions column-actions assist-actions-left" style="opacity: 0">
         <a class="inactive-action" href="javascript:void(0)" data-bind="visible: navigationSettings.showStats, click: showContextPopover, css: { 'blue': statsVisible }"><i class="fa fa-fw fa-info" title="${_('Show details')}"></i></a>
       </div>
       <!-- ko if: expandable -->
-      <a class="assist-entry assist-field-link assist-field-link-dark assist-entry-left-action assist-ellipsis" href="javascript:void(0)" data-bind="click: toggleOpen, attr: {'title': definition.title }">
-        <span data-bind="text: definition.type" class="muted pull-right margin-right-20"></span>
-        <span class="highlightable" data-bind="css: { 'highlight': highlight}, attr: {'column': columnName, 'table': tableName, 'database': databaseName }, text: definition.name, draggableText: { text: editorText, meta: {'type': 'sql', 'column': columnName, 'table': tableName, 'database': databaseName } }"></span><!-- ko if: definition.primary_key === 'true' --> <i class="fa fa-key"></i><!-- /ko -->
+      <a class="assist-entry assist-field-link assist-field-link-dark assist-entry-left-action assist-ellipsis" href="javascript:void(0)" data-bind="click: toggleOpen, attr: {'title': catalogEntry.getTitle() }">
+        <span data-bind="text: catalogEntry.getType()" class="muted pull-right margin-right-20"></span>
+        <span class="highlightable" data-bind="css: { 'highlight': highlight}, attr: {'column': columnName, 'table': tableName, 'database': databaseName }, text: catalogEntry.name, draggableText: { text: editorText, meta: {'type': 'sql', 'column': columnName, 'table': tableName, 'database': databaseName } }"></span><!-- ko if: catalogEntry.isPrimaryKey() --> <i class="fa fa-key"></i><!-- /ko -->
       </a>
       <!-- /ko -->
       <!-- ko ifnot: expandable -->
       <div class="assist-entry assist-field-link assist-field-link-dark default-cursor assist-ellipsis" href="javascript:void(0)" data-bind="event: { dblclick: dblClick }, attr: {'title': definition.title }, css: { 'assist-entry-left-action': navigationSettings.rightAssist }">
-        <span data-bind="text: definition.type" class="muted pull-right margin-right-20"></span>
-        <span class="highlightable" data-bind="css: { 'highlight': highlight}, attr: {'column': columnName, 'table': tableName, 'database': databaseName}, text: definition.name, draggableText: { text: editorText, meta: {'type': 'sql', 'column': columnName, 'table': tableName, 'database': databaseName} }"></span><!-- ko if: definition.primary_key === 'true'  --> <i class="fa fa-key"></i><!-- /ko --><!-- ko if: assistDbSource.activeSort() === 'popular' && popularity() > 0 --> <i title="${ _('Popular') }" class="fa fa-star-o top-star"></i> <!-- /ko -->
+        <span data-bind="text: catalogEntry.getType()" class="muted pull-right margin-right-20"></span>
+        <span class="highlightable" data-bind="css: { 'highlight': highlight}, attr: {'column': columnName, 'table': tableName, 'database': databaseName}, text: catalogEntry.getName(), draggableText: { text: editorText, meta: {'type': 'sql', 'column': columnName, 'table': tableName, 'database': databaseName} }"></span><!-- ko if: catalogEntry.isPrimaryKey() --> <i class="fa fa-key"></i><!-- /ko --><!-- ko if: assistDbSource.activeSort() === 'popular' && popularity() > 0 --> <i title="${ _('Popular') }" class="fa fa-star-o top-star"></i> <!-- /ko -->
       </div>
       <!-- /ko -->
       <div class="center assist-spinner" data-bind="visible: loading"><i class="fa fa-spinner fa-spin"></i></div>
@@ -267,28 +267,28 @@ from desktop.views import _ko
   <script type="text/html" id="assist-db-entries">
     <!-- ko if: ! hasErrors() && hasEntries() && ! loading() && filteredEntries().length == 0 -->
     <ul class="assist-tables">
-      <li class="assist-entry assist-no-entries"><!-- ko if: definition.isTable -->${_('No columns found')}<!--/ko--><!-- ko if: definition.isDatabase -->${_('No tables found')}<!--/ko--><!-- ko if: !definition.isTable && !definition.isDatabase -->${_('No results found')}<!--/ko--></li>
+      <li class="assist-entry assist-no-entries"><!-- ko if: catalogEntry.isTable() -->${_('No columns found')}<!--/ko--><!-- ko if: catalogEntry.isDatabase() -->${_('No tables found')}<!--/ko--><!-- ko if: catalogEntry.isField() -->${_('No results found')}<!--/ko--></li>
     </ul>
     <!-- /ko -->
     <!-- ko if: ! hasErrors() && hasEntries() && ! loading() && filteredEntries().length > 0 -->
-    <ul class="database-tree" data-bind="foreachVisible: { data: filteredEntries, minHeight: navigationSettings.rightAssist ? 22 : 23, container: '.assist-db-scrollable', skipScrollEvent: navigationSettings.rightAssist }, css: { 'assist-tables': definition.isDatabase }">
-      <!-- ko template: { if: definition.isTable || definition.isView, name: 'assist-table-entry' } --><!-- /ko -->
+    <ul class="database-tree" data-bind="foreachVisible: { data: filteredEntries, minHeight: navigationSettings.rightAssist ? 22 : 23, container: '.assist-db-scrollable', skipScrollEvent: navigationSettings.rightAssist }, css: { 'assist-tables': catalogEntry.isDatabase() }">
+      <!-- ko template: { if: catalogEntry.isTableOrView(), name: 'assist-table-entry' } --><!-- /ko -->
       <!-- ko if: navigationSettings.rightAssist -->
-        <!-- ko template: { ifnot: definition.isTable || definition.isView, name: 'assist-column-entry-assistant' } --><!-- /ko -->
+        <!-- ko template: { ifnot: catalogEntry.isTableOrView(), name: 'assist-column-entry-assistant' } --><!-- /ko -->
       <!-- /ko -->
       <!-- ko ifnot: navigationSettings.rightAssist -->
-        <!-- ko template: { ifnot: definition.isTable || definition.isView, name: 'assist-column-entry' } --><!-- /ko -->
+        <!-- ko template: { ifnot: catalogEntry.isTableOrView(), name: 'assist-column-entry' } --><!-- /ko -->
       <!-- /ko -->
     </ul>
     <!-- /ko -->
-    <!-- ko template: { if: ! hasErrors() && ! hasEntries() && ! loading() && (definition.isTable || definition.isView), name: 'assist-no-table-entries' } --><!-- /ko -->
-    <!-- ko template: { if: ! hasErrors() && ! hasEntries() && ! loading() && definition.isDatabase, name: 'assist-no-database-entries' } --><!-- /ko -->
+    <!-- ko template: { if: ! hasErrors() && ! hasEntries() && ! loading() && (catalogEntry.isTableOrView()), name: 'assist-no-table-entries' } --><!-- /ko -->
+    <!-- ko template: { if: ! hasErrors() && ! hasEntries() && ! loading() && catalogEntry.isDatabase(), name: 'assist-no-database-entries' } --><!-- /ko -->
     <!-- ko if: hasErrors -->
     <ul class="assist-tables">
-      <!-- ko if: definition.isTable -->
+      <!-- ko if: catalogEntry.isTableOrView() -->
       <li class="assist-errors">${ _('Error loading columns.') }</li>
       <!-- /ko -->
-      <!-- ko ifnot: definition.isTable -->
+      <!-- ko if: catalogEntry.isField() -->
       <li class="assist-errors">${ _('Error loading fields.') }</li>
       <!-- /ko -->
     </ul>
@@ -1301,7 +1301,7 @@ from desktop.views import _ko
         self.breadcrumb = ko.computed(function () {
           if (self.selectedSource()) {
             if (self.selectedSource().selectedDatabase()) {
-              return self.selectedSource().selectedDatabase().definition.name;
+              return self.selectedSource().selectedDatabase().catalogEntry.name;
             }
             return self.selectedSource().name;
           }
@@ -2319,13 +2319,13 @@ from desktop.views import _ko
             <li class="assist-table hue-warning" data-bind="attr: { 'title': $parent.isSolr() ? '${ _ko('Error loading collection details.') }' : '${ _ko('Error loading table details.') }'}">
               <span class="assist-entry">
                 <i class="hue-warning fa fa-fw muted valign-middle fa-warning"></i>
-                <span data-bind="text: definition.displayName"></span>
+                <span data-bind="text: catalogEntry.getDisplayName()"></span>
               </span>
             </li>
             <!-- /ko -->
             <!-- ko ifnot: hasErrors -->
-            <!-- ko template: { if: definition.isTable || definition.isView, name: 'assist-table-entry' } --><!-- /ko -->
-            <!-- ko template: { ifnot: definition.isTable || definition.isView, name: 'assist-column-entry-assistant' } --><!-- /ko -->
+            <!-- ko template: { if: catalogEntry.isTableOrView(), name: 'assist-table-entry' } --><!-- /ko -->
+            <!-- ko template: { if: catalogEntry.isField(), name: 'assist-column-entry-assistant' } --><!-- /ko -->
             <!-- /ko -->
           </ul>
           <!-- /ko -->
@@ -2387,15 +2387,15 @@ from desktop.views import _ko
                 $.each(vm.activeTables(), function (index, entry) {
                   var facetMatch = !facets || !facets['type'] || (!facets['type']['table'] && !facets['type']['view']);
                   if (!facetMatch && facets['type']['table']) {
-                    facetMatch = entry.definition.isTable;
+                    facetMatch = entry.catalogEntry.isTable();
                   }
                   if (!facetMatch && facets['type']['view']) {
-                    facetMatch = entry.definition.isView;
+                    facetMatch = entry.catalogEntry.isView();
                   }
 
                   var textMatch = !vm.filter.querySpec().text || vm.filter.querySpec().text.length === 0;
                   if (!textMatch) {
-                    var nameLower = entry.definition.name.toLowerCase();
+                    var nameLower = entry.catalogEntry.name.toLowerCase();
                     textMatch = vm.filter.querySpec().text.every(function (text) {
                       return nameLower.indexOf(text.toLowerCase()) !== -1;
                     });
@@ -2533,14 +2533,14 @@ from desktop.views import _ko
           var result = [];
           var partialLower = partial.toLowerCase();
           self.activeTables().forEach(function (table) {
-            if (!added[table.definition.name] && table.definition.name.toLowerCase().indexOf(partialLower) === 0) {
-              added[table.definition.name] = true;
-              result.push(nonPartial + partial + table.definition.name.substring(partial.length))
+            if (!added[table.catalogEntry.name] && table.catalogEntry.name.toLowerCase().indexOf(partialLower) === 0) {
+              added[table.catalogEntry.name] = true;
+              result.push(nonPartial + partial + table.catalogEntry.name.substring(partial.length))
             }
             table.entries().forEach(function (col) {
-              if (!added[col.definition.name] && col.definition.name.toLowerCase().indexOf(partialLower) === 0) {
-                added[col.definition.name] = true;
-                result.push(nonPartial + partial + col.definition.name.substring(partial.length))
+              if (!added[col.catalogEntry.name] && col.catalogEntry.name.toLowerCase().indexOf(partialLower) === 0) {
+                added[col.catalogEntry.name] = true;
+                result.push(nonPartial + partial + col.catalogEntry.name.substring(partial.length))
               }
             })
           });
@@ -2634,7 +2634,7 @@ from desktop.views import _ko
               });
 
               tables.sort(function (a, b) {
-                return a.definition.name.localeCompare(b.definition.name);
+                return a.catalogEntry.name.localeCompare(b.catalogEntry.name);
               });
               self.activeTables(tables);
             }
@@ -2935,14 +2935,14 @@ from desktop.views import _ko
             var result = [];
             var partialLower = partial.toLowerCase();
             self.activeTables().forEach(function (table) {
-              if (!added[table.definition.name] && table.definition.name.toLowerCase().indexOf(partialLower) === 0) {
-                added[table.definition.name] = true;
-                result.push(nonPartial + partial + table.definition.name.substring(partial.length))
+              if (!added[table.catalogEntry.name] && table.catalogEntry.name.toLowerCase().indexOf(partialLower) === 0) {
+                added[table.catalogEntry.name] = true;
+                result.push(nonPartial + partial + table.catalogEntry.name.substring(partial.length))
               }
               table.entries().forEach(function (col) {
-                if (!added[col.definition.name] && col.definition.name.toLowerCase().indexOf(partialLower) === 0) {
-                  added[col.definition.name] = true;
-                  result.push(nonPartial + partial + col.definition.name.substring(partial.length))
+                if (!added[col.catalogEntry.name] && col.catalogEntry.name.toLowerCase().indexOf(partialLower) === 0) {
+                  added[col.catalogEntry.name] = true;
+                  result.push(nonPartial + partial + col.catalogEntry.name.substring(partial.length))
                 }
               })
             });