Browse Source

[catalog] Move the data catalog code to typescript and switch form jQuery deferrals to Promises

This is a rather big change but it allows us to use proper promises when working with the data catalog and enables us to switch over to Axios easier.

This also takes care of a couple of "undefined exception" bugs related to the catalog and it's now 100% typed.
Johan Ahlen 5 years ago
parent
commit
368afbe6c9
62 changed files with 3792 additions and 4058 deletions
  1. 2 2
      apps/pig/src/pig/templates/app.mako
  2. 5 7
      desktop/core/src/desktop/js/api/utils.ts
  3. 3 3
      desktop/core/src/desktop/js/apps/notebook/snippet.js
  4. 2 1
      desktop/core/src/desktop/js/apps/notebook2/components/aceEditor/AceEditor.test.ts
  5. 34 34
      desktop/core/src/desktop/js/apps/notebook2/components/aceEditor/AceLocationHandler.ts
  6. 135 105
      desktop/core/src/desktop/js/apps/notebook2/components/aceEditor/autocomplete/AutocompleteResults.ts
  7. 1 1
      desktop/core/src/desktop/js/apps/notebook2/components/aceEditor/autocomplete/CatalogEntryDetailsPanel.vue
  8. 5 5
      desktop/core/src/desktop/js/apps/notebook2/variableSubstitution.ts
  9. 1 1
      desktop/core/src/desktop/js/apps/tableBrowser/app.js
  10. 5 5
      desktop/core/src/desktop/js/apps/tableBrowser/metastoreColumn.js
  11. 16 15
      desktop/core/src/desktop/js/apps/tableBrowser/metastoreDatabase.js
  12. 3 3
      desktop/core/src/desktop/js/apps/tableBrowser/metastoreNamespace.js
  13. 22 21
      desktop/core/src/desktop/js/apps/tableBrowser/metastoreTable.js
  14. 2 2
      desktop/core/src/desktop/js/apps/tableBrowser/metastoreTablePartitions.js
  15. 3 3
      desktop/core/src/desktop/js/apps/tableBrowser/metastoreTableSamples.js
  16. 2 1
      desktop/core/src/desktop/js/apps/tableBrowser/metastoreViewModel.js
  17. 1826 0
      desktop/core/src/desktop/js/catalog/DataCatalogEntry.ts
  18. 137 0
      desktop/core/src/desktop/js/catalog/GeneralDataCatalog.ts
  19. 280 0
      desktop/core/src/desktop/js/catalog/MultiTableEntry.ts
  20. 38 30
      desktop/core/src/desktop/js/catalog/catalogUtils.ts
  21. 0 972
      desktop/core/src/desktop/js/catalog/dataCatalog.js
  22. 880 0
      desktop/core/src/desktop/js/catalog/dataCatalog.ts
  23. 0 68
      desktop/core/src/desktop/js/catalog/dataCatalog/index.d.ts
  24. 0 1840
      desktop/core/src/desktop/js/catalog/dataCatalogEntry.js
  25. 0 131
      desktop/core/src/desktop/js/catalog/dataCatalogEntry/index.d.ts
  26. 0 139
      desktop/core/src/desktop/js/catalog/generalDataCatalog.js
  27. 0 299
      desktop/core/src/desktop/js/catalog/multiTableEntry.js
  28. 2 2
      desktop/core/src/desktop/js/catalog/optimizer/localStrategy.js
  29. 10 8
      desktop/core/src/desktop/js/jquery/plugins/jquery.hiveautocomplete.js
  30. 9 9
      desktop/core/src/desktop/js/ko/bindings/ace/aceLocationHandler.js
  31. 1 1
      desktop/core/src/desktop/js/ko/bindings/ko.sqlContextPopover.js
  32. 1 1
      desktop/core/src/desktop/js/ko/bindings/ko.storageContextPopover.js
  33. 10 8
      desktop/core/src/desktop/js/ko/components/assist/assistDbEntry.js
  34. 8 6
      desktop/core/src/desktop/js/ko/components/assist/assistDbNamespace.js
  35. 2 2
      desktop/core/src/desktop/js/ko/components/assist/ko.assistDashboardPanel.js
  36. 1 1
      desktop/core/src/desktop/js/ko/components/assist/ko.assistDbPanel.js
  37. 12 12
      desktop/core/src/desktop/js/ko/components/assist/ko.assistEditorContextPanel.js
  38. 8 5
      desktop/core/src/desktop/js/ko/components/assist/ko.assistKey.js
  39. 4 4
      desktop/core/src/desktop/js/ko/components/contextPopover/asteriskContextTabs.js
  40. 9 9
      desktop/core/src/desktop/js/ko/components/contextPopover/dataCatalogContext.js
  41. 5 2
      desktop/core/src/desktop/js/ko/components/contextPopover/ko.contextPopover.js
  42. 1 1
      desktop/core/src/desktop/js/ko/components/contextPopover/storageContext.js
  43. 53 52
      desktop/core/src/desktop/js/ko/components/ko.catalogEntriesList.js
  44. 7 5
      desktop/core/src/desktop/js/ko/components/ko.catalogEntriesList.test.js
  45. 4 4
      desktop/core/src/desktop/js/ko/components/ko.contextSelector.js
  46. 2 2
      desktop/core/src/desktop/js/ko/components/ko.executionAnalysis.js
  47. 24 26
      desktop/core/src/desktop/js/ko/components/ko.fieldSamples.js
  48. 6 7
      desktop/core/src/desktop/js/ko/components/ko.fieldSamples.test.js
  49. 4 4
      desktop/core/src/desktop/js/ko/components/ko.globalSearch.js
  50. 1 1
      desktop/core/src/desktop/js/ko/components/ko.historyPanel.js
  51. 6 6
      desktop/core/src/desktop/js/ko/components/ko.navProperties.js
  52. 6 6
      desktop/core/src/desktop/js/ko/components/ko.navProperties.test.js
  53. 11 11
      desktop/core/src/desktop/js/ko/components/ko.navTags.js
  54. 8 6
      desktop/core/src/desktop/js/ko/components/ko.navTags.test.js
  55. 3 3
      desktop/core/src/desktop/js/ko/components/ko.pollingCatalogEntriesList.js
  56. 4 4
      desktop/core/src/desktop/js/parse/types.ts
  57. 60 60
      desktop/core/src/desktop/js/sql/autocompleteResults.js
  58. 87 83
      desktop/core/src/desktop/js/sql/sqlUtils.ts
  59. 2 2
      desktop/core/src/desktop/js/sql/sqlWorkerHandler.js
  60. 8 8
      desktop/core/src/desktop/static/desktop/js/jquery.hiveautocomplete.js
  61. 4 2
      desktop/core/src/desktop/templates/hue_ace_autocompleter.mako
  62. 7 7
      desktop/libs/indexer/src/indexer/templates/importer.mako

+ 2 - 2
apps/pig/src/pig/templates/app.mako

@@ -1025,9 +1025,9 @@ ${ commonshare() | n,unicode }
           connector: { id: 'hive' },
           path: ['default'],
           silenceErrors: true
-        }).done(function (childEntries) {
+        }).then(function (childEntries) {
           availableTables = $.map(childEntries, function (entry) { return entry.name }).join(' ');
-        });
+        }).catch(function() {});
       });
     % endif
 

+ 5 - 7
desktop/core/src/desktop/js/api/utils.ts

@@ -120,13 +120,11 @@ export const post = <T, U = unknown>(
         completed = true;
       });
 
-    if (onCancel) {
-      onCancel(() => {
-        if (!completed) {
-          cancelTokenSource.cancel();
-        }
-      });
-    }
+    onCancel(() => {
+      if (!completed) {
+        cancelTokenSource.cancel();
+      }
+    });
   });
 
 export const cancelActiveRequest = (request?: JQuery.jqXHR): void => {

+ 3 - 3
desktop/core/src/desktop/js/apps/notebook/snippet.js

@@ -941,7 +941,7 @@ class Snippet {
       self.variables().forEach(variable => {
         if (oLocations[variable.name()]) {
           activeSourcePromises.push(
-            oLocations[variable.name()].resolveCatalogEntry({ cancellable: true }).done(entry => {
+            oLocations[variable.name()].resolveCatalogEntry({ cancellable: true }).then(entry => {
               variable.path(entry.path.join('.'));
               variable.catalogEntry = entry;
 
@@ -1086,8 +1086,8 @@ class Snippet {
                 connector: self.connector(),
                 path: path
               })
-              .done(entry => {
-                entry.clearCache({ refreshCache: true, cascade: true, silenceErrors: true });
+              .then(entry => {
+                entry.clearCache({ cascade: true, silenceErrors: true });
               });
           }, 5000);
         }

+ 2 - 1
desktop/core/src/desktop/js/apps/notebook2/components/aceEditor/AceEditor.test.ts

@@ -15,12 +15,13 @@
 // limitations under the License.
 
 import { shallowMount } from '@vue/test-utils';
+import { CancellablePromise } from 'api/cancellablePromise';
 import dataCatalog from 'catalog/dataCatalog';
 import AceEditor from './AceEditor.vue';
 
 describe('AceEditor.vue', () => {
   it('should render', () => {
-    spyOn(dataCatalog, 'getChildren').and.returnValue(Promise.resolve([]));
+    spyOn(dataCatalog, 'getChildren').and.returnValue(CancellablePromise.resolve([]));
 
     const wrapper = shallowMount(AceEditor, {
       propsData: {

+ 34 - 34
desktop/core/src/desktop/js/apps/notebook2/components/aceEditor/AceLocationHandler.ts

@@ -18,7 +18,7 @@ import { Ace } from 'ext/ace';
 import ace from 'ext/aceHelper';
 
 import Executor from 'apps/notebook2/execution/executor';
-import DataCatalogEntry from 'catalog/dataCatalogEntry';
+import DataCatalogEntry, { TableSourceMeta } from 'catalog/DataCatalogEntry';
 import SubscriptionTracker, { Disposable } from 'components/utils/SubscriptionTracker';
 import AssistStorageEntry from 'ko/components/assist/assistStorageEntry';
 import dataCatalog from 'catalog/dataCatalog';
@@ -146,25 +146,25 @@ export default class AceLocationHandler implements Disposable {
     this.updateAvailableDatabases();
   }
 
-  private updateAvailableDatabases() {
+  private async updateAvailableDatabases() {
     window.clearTimeout(this.updateTimeout);
     if (!this.executor.namespace() || !this.executor.compute()) {
       this.updateTimeout = window.setTimeout(this.updateAvailableDatabases.bind(this), 300);
       return;
     }
-    dataCatalog
-      .getChildren({
+    try {
+      const children = await dataCatalog.getChildren({
         connector: this.executor.connector(),
         namespace: this.executor.namespace(),
         compute: this.executor.compute(),
         path: <string[]>[]
-      })
-      .then(children => {
-        this.availableDatabases.clear();
-        children.forEach((dbEntry: DataCatalogEntry) => {
-          this.availableDatabases.add(dbEntry.name.toLowerCase());
-        });
       });
+
+      this.availableDatabases.clear();
+      children.forEach((dbEntry: DataCatalogEntry) => {
+        this.availableDatabases.add(dbEntry.name.toLowerCase());
+      });
+    } catch (err) {}
   }
 
   private isSqlDialect(): boolean {
@@ -282,12 +282,12 @@ export default class AceLocationHandler implements Disposable {
                         temporaryOnly: this.temporaryOnly,
                         path: tableChain.map(identifier => identifier.name)
                       })
-                      .done(entry => {
+                      .then(entry => {
                         entry
                           .getSourceMeta({ cachedOnly: true, silenceErrors: true })
-                          .done(sourceMeta => {
-                            if (sourceMeta && sourceMeta.extended_columns) {
-                              sourceMeta.extended_columns.every(
+                          .then(sourceMeta => {
+                            if (sourceMeta && (<TableSourceMeta>sourceMeta).extended_columns) {
+                              (<TableSourceMeta>sourceMeta).extended_columns.every(
                                 (col: { name: string; type: string }) => {
                                   if (col.name.toLowerCase() === colName) {
                                     colType = (col.type.match(/^[^<]*/g) || ['T'])[0];
@@ -468,7 +468,7 @@ export default class AceLocationHandler implements Disposable {
               .resolveCatalogEntry({
                 temporaryOnly: this.temporaryOnly
               })
-              .done(entry => {
+              .then(entry => {
                 huePubSub.publish('context.popover.show', {
                   data: {
                     type: 'catalogEntry',
@@ -479,7 +479,7 @@ export default class AceLocationHandler implements Disposable {
                   source: source
                 });
               })
-              .fail(() => {
+              .catch(() => {
                 token.notFound = true;
               });
           } else if (token.parseLocation && !token.notFound) {
@@ -903,19 +903,14 @@ export default class AceLocationHandler implements Disposable {
   }
 
   async fetchChildren(identifierChain: IdentifierChainEntry[]): Promise<DataCatalogEntry[]> {
-    return new Promise((resolve, reject) => {
-      dataCatalog
-        .getChildren({
-          connector: this.executor.connector(),
-          namespace: this.executor.namespace(),
-          compute: this.executor.compute(),
-          temporaryOnly: this.temporaryOnly,
-          path: identifierChain.map(identifier => identifier.name),
-          silenceErrors: true,
-          cachedOnly: true
-        })
-        .done(resolve)
-        .fail(reject);
+    return dataCatalog.getChildren({
+      connector: this.executor.connector(),
+      namespace: this.executor.namespace(),
+      compute: this.executor.compute(),
+      temporaryOnly: this.temporaryOnly,
+      path: identifierChain.map(identifier => identifier.name),
+      silenceErrors: true,
+      cachedOnly: true
     });
   }
 
@@ -960,9 +955,14 @@ export default class AceLocationHandler implements Disposable {
       token.parseLocation.identifierChain.length
     ) {
       // fetch the parent
-      return await this.fetchChildren(
-        token.parseLocation.identifierChain.slice(0, token.parseLocation.identifierChain.length - 1)
-      );
+      try {
+        return await this.fetchChildren(
+          token.parseLocation.identifierChain.slice(
+            0,
+            token.parseLocation.identifierChain.length - 1
+          )
+        );
+      } catch (err) {}
     }
 
     return [];
@@ -1035,7 +1035,7 @@ export default class AceLocationHandler implements Disposable {
                   cachedOnly: true,
                   silenceErrors: true
                 })
-                .done((entries: DataCatalogEntry[]) => {
+                .then((entries: DataCatalogEntry[]) => {
                   const containsColumn = entries.some(
                     entry =>
                       location.identifierChain &&
@@ -1055,7 +1055,7 @@ export default class AceLocationHandler implements Disposable {
                     resolve();
                   }
                 })
-                .fail(() => resolve());
+                .catch(() => resolve());
             } else if (tablesToGo.length > 0) {
               findIdentifierChainInTable(tablesToGo);
             } else {

+ 135 - 105
desktop/core/src/desktop/js/apps/notebook2/components/aceEditor/autocomplete/AutocompleteResults.ts

@@ -14,20 +14,24 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import CancellableJqPromise from 'api/cancellableJqPromise';
+import { CancellablePromise } from 'api/cancellablePromise';
 import { Category, CategoryInfo } from 'apps/notebook2/components/aceEditor/autocomplete/Category';
 import Executor from 'apps/notebook2/execution/executor';
 import DataCatalogEntry, {
-  Field,
-  OptimizerPopularity,
+  FieldSample,
+  FieldSourceMeta,
   SourceMeta,
+  TableSourceMeta
+} from 'catalog/DataCatalogEntry';
+import { OptimizerPopularity, OptimizerPopularitySubType } from 'catalog/dataCatalog';
+import MultiTableEntry, {
   TopAggs,
   TopAggValue,
   TopFilters,
   TopFilterValue,
   TopJoins,
   TopJoinValue
-} from 'catalog/dataCatalogEntry';
+} from 'catalog/MultiTableEntry';
 import SubscriptionTracker from 'components/utils/SubscriptionTracker';
 import { Ace } from 'ext/ace';
 import {
@@ -60,7 +64,7 @@ import {
 
 interface ColumnReference {
   type: string;
-  samples?: (string | number)[];
+  sample?: FieldSample[][];
 }
 
 export interface ColRefKeywordDetails {
@@ -90,7 +94,7 @@ export interface Suggestion {
     | TopJoinValue
     | TopAggValue
     | TopFilterValue
-    | Field
+    | FieldSample
     | CommentDetails
     | { name: string };
   matchComment?: boolean;
@@ -148,7 +152,7 @@ class AutocompleteResults {
   subTracker = new SubscriptionTracker();
   onCancelFunctions: (() => void)[] = [];
   lastKnownRequests: JQueryXHR[] = [];
-  cancellablePromises: CancellableJqPromise<unknown>[] = [];
+  cancellablePromises: CancellablePromise<unknown>[] = [];
 
   constructor(options: { executor: Executor; editor: Ace.Editor; temporaryOnly: boolean }) {
     this.executor = options.executor;
@@ -292,15 +296,18 @@ class AutocompleteResults {
         if (catalogEntry) {
           const sourceMeta = await new Promise<SourceMeta>((resolve, reject) => {
             this.onCancelFunctions.push(reject);
-            const sourceMetaDeferred = catalogEntry.getSourceMeta({
+            const sourceMetaPromise = catalogEntry.getSourceMeta({
               silenceErrors: true,
               cancellable: true
             });
-            this.cancellablePromises.push(sourceMetaDeferred);
-            sourceMetaDeferred.done(resolve).fail(reject);
+            this.cancellablePromises.push(sourceMetaPromise);
+            sourceMetaPromise.then(resolve).catch(reject);
           });
-          if (sourceMeta.type) {
-            return { type: sourceMeta.type, samples: sourceMeta.samples }; // sourceMeta.samples is used in handleValues.
+          if ((<FieldSourceMeta>sourceMeta).type) {
+            return {
+              type: (<FieldSourceMeta>sourceMeta).type,
+              sample: (<FieldSourceMeta>sourceMeta).sample // sourceMeta.sample is used in handleValues.
+            };
           }
         }
       } catch (err) {}
@@ -320,15 +327,15 @@ class AutocompleteResults {
             path: [],
             temporaryOnly: this.temporaryOnly
           })
-          .done(resolve)
-          .fail(reject);
+          .then(resolve)
+          .catch(reject);
       });
 
       return await new Promise((resolve, reject) => {
         this.onCancelFunctions.push(reject);
-        const childrenDeferred = entry.getChildren({ silenceErrors: true, cancellable: true });
-        this.cancellablePromises.push(childrenDeferred);
-        childrenDeferred.done(resolve).fail(reject);
+        const childrenPromise = entry.getChildren({ silenceErrors: true, cancellable: true });
+        this.cancellablePromises.push(childrenPromise);
+        childrenPromise.then(resolve).catch(reject);
       });
     } catch (err) {
       return [];
@@ -618,15 +625,15 @@ class AutocompleteResults {
               path: [database],
               temporaryOnly: this.temporaryOnly
             })
-            .done(resolve)
-            .fail(reject);
+            .then(resolve)
+            .catch(reject);
         });
 
         const tableEntries = await new Promise<DataCatalogEntry[]>((resolve, reject) => {
           this.onCancelFunctions.push(reject);
-          const childrenDeferred = dbEntry.getChildren({ silenceErrors: true, cancellable: true });
-          this.cancellablePromises.push(childrenDeferred);
-          childrenDeferred.done(resolve).fail(reject);
+          const childrenPromise = dbEntry.getChildren({ silenceErrors: true, cancellable: true });
+          this.cancellablePromises.push(childrenPromise);
+          childrenPromise.then(resolve).catch(reject);
         });
 
         for (const tableEntry of tableEntries) {
@@ -869,24 +876,24 @@ class AutocompleteResults {
       await this.addSubQueryColumns(table, columnSuggestions);
     } else if (typeof table.identifierChain !== 'undefined') {
       const addColumnsFromEntry = async (dataCatalogEntry: DataCatalogEntry): Promise<void> => {
-        const sourceMeta = await new Promise<SourceMeta>((resolve, reject) => {
+        const sourceMeta = (await new Promise<SourceMeta>((resolve, reject) => {
           this.onCancelFunctions.push(reject);
-          const sourceMetaDeferred = dataCatalogEntry.getSourceMeta({
+          const sourceMetaPromise = dataCatalogEntry.getSourceMeta({
             silenceErrors: true,
             cancellable: true
           });
-          this.cancellablePromises.push(sourceMetaDeferred);
-          sourceMetaDeferred.done(resolve).fail(reject);
-        });
+          this.cancellablePromises.push(sourceMetaPromise);
+          sourceMetaPromise.then(resolve).catch(reject);
+        })) as FieldSourceMeta;
 
         const childEntries = await new Promise<DataCatalogEntry[]>((resolve, reject) => {
           this.onCancelFunctions.push(reject);
-          const childrenDeferred = dataCatalogEntry.getChildren({
+          const childrenPromise = dataCatalogEntry.getChildren({
             silenceErrors: true,
             cancellable: true
           });
-          this.cancellablePromises.push(childrenDeferred);
-          childrenDeferred.done(resolve).fail(reject);
+          this.cancellablePromises.push(childrenPromise);
+          childrenPromise.then(resolve).catch(reject);
         });
 
         for (const childEntry of childEntries) {
@@ -1035,16 +1042,16 @@ class AutocompleteResults {
 
     const colRef = await colRefPromise;
 
-    if (colRef.samples) {
+    if (colRef.sample) {
       const isString = colRef.type === 'string';
       const startQuote = suggestValues.partialQuote ? '' : "'";
       const endQuote =
         typeof suggestValues.missingEndQuote !== 'undefined' && !suggestValues.missingEndQuote
           ? ''
           : suggestValues.partialQuote || "'";
-      colRef.samples.forEach(sample => {
+      colRef.sample.forEach(sample => {
         valueSuggestions.push({
-          value: isString ? startQuote + sample + endQuote : String(sample),
+          value: isString ? startQuote + sample[0] + endQuote : String(sample[0]),
           meta: MetaLabels.Sample,
           category: Category.Sample,
           popular: false
@@ -1207,7 +1214,7 @@ class AutocompleteResults {
 
     const joinSuggestions: Suggestion[] = [];
     try {
-      const multiTableEntry = await new Promise<DataCatalogEntry>((resolve, reject) => {
+      const multiTableEntry = await new Promise<MultiTableEntry>((resolve, reject) => {
         this.onCancelFunctions.push(reject);
         dataCatalog
           .getMultiTableEntry({
@@ -1216,18 +1223,18 @@ class AutocompleteResults {
             connector: this.executor.connector(),
             paths: paths
           })
-          .done(resolve)
-          .fail(reject);
+          .then(resolve)
+          .catch(reject);
       });
 
       const topJoins = await new Promise<TopJoins>((resolve, reject) => {
         this.onCancelFunctions.push(reject);
-        const topJoinsDeferred = multiTableEntry.getTopJoins({
+        const topJoinsPromise = multiTableEntry.getTopJoins({
           silenceErrors: true,
           cancellable: true
         });
-        this.cancellablePromises.push(topJoinsDeferred);
-        topJoinsDeferred.done(resolve).fail(reject);
+        this.cancellablePromises.push(topJoinsPromise);
+        topJoinsPromise.then(resolve).catch(reject);
       });
 
       let totalCount = 0;
@@ -1328,7 +1335,7 @@ class AutocompleteResults {
     const joinConditionSuggestions: Suggestion[] = [];
 
     try {
-      const multiTableEntry = await new Promise<DataCatalogEntry>((resolve, reject) => {
+      const multiTableEntry = await new Promise<MultiTableEntry>((resolve, reject) => {
         this.onCancelFunctions.push(reject);
         dataCatalog
           .getMultiTableEntry({
@@ -1337,18 +1344,18 @@ class AutocompleteResults {
             connector: this.executor.connector(),
             paths: paths
           })
-          .done(resolve)
-          .fail(reject);
+          .then(resolve)
+          .catch(reject);
       });
 
       const topJoins = await new Promise<TopJoins>((resolve, reject) => {
         this.onCancelFunctions.push(reject);
-        const topJoinsDeferred = multiTableEntry.getTopJoins({
+        const topJoinsPromise = multiTableEntry.getTopJoins({
           silenceErrors: true,
           cancellable: true
         });
-        this.cancellablePromises.push(topJoinsDeferred);
-        topJoinsDeferred.done(resolve).fail(reject);
+        this.cancellablePromises.push(topJoinsPromise);
+        topJoinsPromise.then(resolve).catch(reject);
       });
 
       let totalCount = 0;
@@ -1419,7 +1426,7 @@ class AutocompleteResults {
     const aggregateFunctionsSuggestions: Suggestion[] = [];
 
     try {
-      const multiTableEntry = await new Promise<DataCatalogEntry>((resolve, reject) => {
+      const multiTableEntry = await new Promise<MultiTableEntry>((resolve, reject) => {
         this.onCancelFunctions.push(reject);
         dataCatalog
           .getMultiTableEntry({
@@ -1428,8 +1435,8 @@ class AutocompleteResults {
             connector: this.executor.connector(),
             paths: paths
           })
-          .done(resolve)
-          .fail(reject);
+          .then(resolve)
+          .catch(reject);
       });
 
       const topAggs = await new Promise<TopAggs>((resolve, reject) => {
@@ -1439,7 +1446,7 @@ class AutocompleteResults {
           cancellable: true
         });
         this.cancellablePromises.push(topAggsDeferred);
-        topAggsDeferred.done(resolve).fail(reject);
+        topAggsDeferred.then(resolve).catch(reject);
       });
 
       if (!topAggs.values || !topAggs.values.length) {
@@ -1527,7 +1534,7 @@ class AutocompleteResults {
   }
 
   async handlePopularGroupByOrOrderBy(
-    optimizerAttribute: string,
+    optimizerAttribute: keyof OptimizerPopularitySubType,
     suggestSpec: CommonPopularSuggestion,
     columnsPromise: Promise<Suggestion[]>
   ): Promise<Suggestion[]> {
@@ -1549,7 +1556,7 @@ class AutocompleteResults {
     try {
       const entries = await new Promise<DataCatalogEntry[]>((resolve, reject) => {
         this.onCancelFunctions.push(reject);
-        const popularityDeferred = dataCatalog
+        const popularityPromise = dataCatalog
           .getCatalog(this.executor.connector())
           .loadOptimizerPopularityForTables({
             namespace: this.executor.namespace(),
@@ -1558,8 +1565,8 @@ class AutocompleteResults {
             silenceErrors: true,
             cancellable: true
           });
-        this.cancellablePromises.push(popularityDeferred);
-        popularityDeferred.done(resolve).fail(reject);
+        this.cancellablePromises.push(popularityPromise);
+        popularityPromise.then(resolve).catch(reject);
       });
 
       let totalColumnCount = 0;
@@ -1569,10 +1576,12 @@ class AutocompleteResults {
         : '';
 
       entries.forEach(entry => {
-        if (entry.optimizerPopularity[optimizerAttribute]) {
-          totalColumnCount += (<{ columnCount: number }>(
-            entry.optimizerPopularity[optimizerAttribute]
-          )).columnCount;
+        if (!entry.optimizerPopularity) {
+          return;
+        }
+        const popularity = entry.optimizerPopularity[optimizerAttribute];
+        if (popularity) {
+          totalColumnCount += popularity.columnCount;
           matchedEntries.push(entry);
         }
       });
@@ -1580,8 +1589,13 @@ class AutocompleteResults {
       if (totalColumnCount > 0) {
         const suggestions: Suggestion[] = [];
         matchedEntries.forEach(entry => {
+          const popularity =
+            entry.optimizerPopularity && entry.optimizerPopularity[optimizerAttribute];
+          if (!popularity) {
+            return;
+          }
           const filterValue = this.createOptimizerIdentifierForColumn(
-            <OptimizerPopularity>entry.optimizerPopularity[optimizerAttribute],
+            popularity,
             suggestSpec.tables
           );
           suggestions.push({
@@ -1592,12 +1606,7 @@ class AutocompleteResults {
               optimizerAttribute === 'groupByColumn'
                 ? Category.PopularGroupBy
                 : Category.PopularOrderBy,
-            weightAdjust: Math.round(
-              (100 *
-                (<{ columnCount: number }>entry.optimizerPopularity[optimizerAttribute])
-                  .columnCount) /
-                totalColumnCount
-            ),
+            weightAdjust: Math.round((100 * popularity.columnCount) / totalColumnCount),
             popular: true,
             hasCatalogEntry: false,
             details: entry
@@ -1663,7 +1672,7 @@ class AutocompleteResults {
     const filterSuggestions: Suggestion[] = [];
 
     try {
-      const multiTableEntry = await new Promise<DataCatalogEntry>((resolve, reject) => {
+      const multiTableEntry = await new Promise<MultiTableEntry>((resolve, reject) => {
         this.onCancelFunctions.push(reject);
         dataCatalog
           .getMultiTableEntry({
@@ -1672,18 +1681,18 @@ class AutocompleteResults {
             connector: this.executor.connector(),
             paths: paths
           })
-          .done(resolve)
-          .fail(reject);
+          .then(resolve)
+          .catch(reject);
       });
 
       const topFilters = await new Promise<TopFilters>((resolve, reject) => {
         this.onCancelFunctions.push(reject);
-        const topFiltersDeferred = multiTableEntry.getTopFilters({
+        const topFiltersPromise = multiTableEntry.getTopFilters({
           silenceErrors: true,
           cancellable: true
         });
-        this.cancellablePromises.push(topFiltersDeferred);
-        topFiltersDeferred.done(resolve).fail(reject);
+        this.cancellablePromises.push(topFiltersPromise);
+        topFiltersPromise.then(resolve).catch(reject);
       });
 
       let totalCount = 0;
@@ -1760,18 +1769,18 @@ class AutocompleteResults {
             path: [db],
             temporaryOnly: this.temporaryOnly
           })
-          .done(resolve)
-          .fail(reject);
+          .then(resolve)
+          .catch(reject);
       });
 
       const childEntries = await new Promise<DataCatalogEntry[]>((resolve, reject) => {
         this.onCancelFunctions.push(reject);
-        const popularityDeferred = entry.loadOptimizerPopularityForChildren({
+        const popularityPromise = entry.loadOptimizerPopularityForChildren({
           silenceErrors: true,
           cancellable: true
         });
-        this.cancellablePromises.push(popularityDeferred);
-        popularityDeferred.done(resolve).fail(reject);
+        this.cancellablePromises.push(popularityPromise);
+        popularityPromise.then(resolve).catch(reject);
       });
 
       let totalPopularity = 0;
@@ -1789,9 +1798,9 @@ class AutocompleteResults {
         tableSuggestions.forEach(suggestion => {
           const details = <DataCatalogEntry>suggestion.details;
           if (popularityIndex.has(details.name)) {
-            suggestion.relativePopularity = Math.round(
-              (100 * <number>details.optimizerPopularity.popularity) / totalPopularity
-            );
+            const popularity =
+              (details.optimizerPopularity && details.optimizerPopularity.popularity) || 0;
+            suggestion.relativePopularity = Math.round((100 * popularity) / totalPopularity);
             if (suggestion.relativePopularity >= 5) {
               suggestion.popular = true;
             }
@@ -1838,7 +1847,7 @@ class AutocompleteResults {
 
       const popularEntries = await new Promise<DataCatalogEntry[]>((resolve, reject) => {
         this.onCancelFunctions.push(reject);
-        const popularityDeferred = dataCatalog
+        const popularityPromise = dataCatalog
           .getCatalog(this.executor.connector())
           .loadOptimizerPopularityForTables({
             namespace: this.executor.namespace(),
@@ -1847,11 +1856,13 @@ class AutocompleteResults {
             silenceErrors: true,
             cancellable: true
           });
-        this.cancellablePromises.push(popularityDeferred);
-        popularityDeferred.done(resolve).fail(reject);
+        this.cancellablePromises.push(popularityPromise);
+        popularityPromise.then(resolve).catch(reject);
       });
 
-      let valueAttribute = '';
+      let valueAttribute:
+        | keyof Pick<OptimizerPopularitySubType, 'selectColumn' | 'groupByColumn' | 'orderByColumn'>
+        | undefined;
       switch (suggestColumns.source) {
         case 'select':
           valueAttribute = 'selectColumn';
@@ -1866,7 +1877,11 @@ class AutocompleteResults {
       const popularityIndex = new Set<string>();
 
       popularEntries.forEach(popularEntry => {
-        if (popularEntry.optimizerPopularity && popularEntry.optimizerPopularity[valueAttribute]) {
+        if (
+          valueAttribute &&
+          popularEntry.optimizerPopularity &&
+          popularEntry.optimizerPopularity[valueAttribute]
+        ) {
           popularityIndex.add(popularEntry.getQualifiedPath());
         }
       });
@@ -1879,19 +1894,32 @@ class AutocompleteResults {
       const matchedSuggestions: Suggestion[] = [];
       columnSuggestions.forEach(suggestion => {
         const details = <DataCatalogEntry>suggestion.details;
-        if (suggestion.hasCatalogEntry && popularityIndex.has(details.getQualifiedPath())) {
+        if (!valueAttribute) {
+          return;
+        }
+        const popularity =
+          details.optimizerPopularity && details.optimizerPopularity[valueAttribute];
+        if (
+          popularity &&
+          suggestion.hasCatalogEntry &&
+          popularityIndex.has(details.getQualifiedPath())
+        ) {
           matchedSuggestions.push(suggestion);
-          totalColumnCount += (<{ columnCount: number }>details.optimizerPopularity[valueAttribute])
-            .columnCount;
+          totalColumnCount += popularity.columnCount;
         }
       });
       if (totalColumnCount > 0) {
         matchedSuggestions.forEach(matchedSuggestion => {
           const details = <DataCatalogEntry>matchedSuggestion.details;
+          if (!details.optimizerPopularity || !valueAttribute) {
+            return;
+          }
+          const popularity = details.optimizerPopularity[valueAttribute];
+          if (!popularity) {
+            return;
+          }
           matchedSuggestion.relativePopularity = Math.round(
-            (100 *
-              (<{ columnCount: number }>details.optimizerPopularity[valueAttribute]).columnCount) /
-              totalColumnCount
+            (100 * popularity.columnCount) / totalColumnCount
           );
           if (matchedSuggestion.relativePopularity >= 5) {
             matchedSuggestion.popular = true;
@@ -2072,37 +2100,39 @@ class AutocompleteResults {
             path: <string[]>fetchedPath,
             temporaryOnly: this.temporaryOnly
           })
-          .done(resolve)
-          .fail(reject);
+          .then(resolve)
+          .catch(reject);
       });
 
       const sourceMeta = await new Promise<SourceMeta>((resolve, reject) => {
         this.onCancelFunctions.push(reject);
-        const sourceMetaDeferred = catalogEntry.getSourceMeta({
+        const sourceMetaPromise = catalogEntry.getSourceMeta({
           silenceErrors: true,
           cancellable: true
         });
-        this.cancellablePromises.push(sourceMetaDeferred);
-        sourceMetaDeferred.done(resolve).fail(reject);
+        this.cancellablePromises.push(sourceMetaPromise);
+        sourceMetaPromise.then(resolve).catch(reject);
       });
 
+      const extendedColumns = (<TableSourceMeta>sourceMeta).extended_columns;
       if (
         this.dialect() === HIVE_DIALECT &&
-        typeof sourceMeta.extended_columns !== 'undefined' &&
-        sourceMeta.extended_columns.length === 1 &&
-        /^(?:map|array|struct)/i.test(sourceMeta.extended_columns[0].type)
+        extendedColumns &&
+        extendedColumns.length === 1 &&
+        /^(?:map|array|struct)/i.test(extendedColumns[0].type)
       ) {
-        remainingPath.unshift(sourceMeta.extended_columns[0].name);
+        remainingPath.unshift(extendedColumns[0].name);
       }
+
       if (remainingPath.length) {
         if (/value|item|key/i.test(remainingPath[0])) {
           const path = remainingPath.shift();
           if (path) {
             fetchedPath.push(path);
           }
-        } else if (sourceMeta.type === 'array') {
+        } else if ((<FieldSourceMeta>sourceMeta).type === 'array') {
           fetchedPath.push('item');
-        } else if (sourceMeta.type === 'map') {
+        } else if ((<FieldSourceMeta>sourceMeta).type === 'map') {
           fetchedPath.push('value');
         }
         return await fetchFieldRecursive(remainingPath, fetchedPath);
@@ -2127,18 +2157,18 @@ class AutocompleteResults {
             path: [],
             temporaryOnly: this.temporaryOnly
           })
-          .done(resolve)
-          .fail(reject);
+          .then(resolve)
+          .catch(reject);
       });
 
       const databaseEntries = await new Promise<DataCatalogEntry[]>((resolve, reject) => {
         this.onCancelFunctions.push(reject);
-        const childrenDeferred = catalogEntry.getChildren({
+        const childrenPromise = catalogEntry.getChildren({
           silenceErrors: true,
           cancellable: true
         });
-        this.cancellablePromises.push(childrenDeferred);
-        childrenDeferred.done(resolve).fail(reject);
+        this.cancellablePromises.push(childrenPromise);
+        childrenPromise.then(resolve).catch(reject);
       });
 
       const firstIsDb = databaseEntries.some(dbEntry =>

+ 1 - 1
desktop/core/src/desktop/js/apps/notebook2/components/aceEditor/autocomplete/CatalogEntryDetailsPanel.vue

@@ -70,7 +70,7 @@
   import Component from 'vue-class-component';
   import { Prop } from 'vue-property-decorator';
 
-  import DataCatalogEntry from 'catalog/dataCatalogEntry';
+  import DataCatalogEntry from 'catalog/DataCatalogEntry';
   import I18n from 'utils/i18n';
   import { Suggestion } from './AutocompleteResults';
 

+ 5 - 5
desktop/core/src/desktop/js/apps/notebook2/variableSubstitution.ts

@@ -16,7 +16,7 @@
 
 import { Cancellable } from 'api/cancellablePromise';
 import { DIALECT } from 'apps/notebook2/snippet';
-import DataCatalogEntry from 'catalog/dataCatalogEntry';
+import DataCatalogEntry, { FieldSourceMeta, SourceMeta } from 'catalog/DataCatalogEntry';
 import ko, { PureComputed } from 'knockout';
 import { Observable, ObservableArray } from 'knockout';
 import { IdentifierLocation } from 'parse/types';
@@ -355,10 +355,10 @@ export class VariableSubstitutionHandler {
       }
     }
 
-    const updateVariableType = (variable: Variable, sourceMeta?: { type?: string }) => {
+    const updateVariableType = (variable: Variable, sourceMeta?: SourceMeta) => {
       let type;
-      if (sourceMeta && sourceMeta.type) {
-        type = sourceMeta.type.toLowerCase();
+      if (sourceMeta && (<FieldSourceMeta>sourceMeta).type) {
+        type = (<FieldSourceMeta>sourceMeta).type.toLowerCase();
       } else {
         type = 'string';
       }
@@ -434,7 +434,7 @@ export class VariableSubstitutionHandler {
           });
         });
       } else {
-        updateVariableType(variable, {
+        updateVariableType(variable, <FieldSourceMeta>{
           type: 'text'
         });
       }

+ 1 - 1
desktop/core/src/desktop/js/apps/tableBrowser/app.js

@@ -155,7 +155,7 @@ huePubSub.subscribe('app.dom.loaded', app => {
         path: [],
         definition: { type: 'source' }
       })
-      .done(entry => {
+      .then(entry => {
         entry.clearCache({
           silenceErrors: true
         });

+ 5 - 5
desktop/core/src/desktop/js/apps/tableBrowser/metastoreColumn.js

@@ -35,20 +35,20 @@ class MetastoreColumn {
     this.comment = ko.observable();
 
     this.comment.subscribe(newValue => {
-      this.catalogEntry.getComment().done(comment => {
+      this.catalogEntry.getComment().then(comment => {
         if (comment !== newValue) {
           this.catalogEntry
             .setComment(newValue)
-            .done(this.comment)
-            .fail(() => {
+            .then(this.comment)
+            .catch(() => {
               this.comment(comment);
             });
         }
       });
     });
 
-    this.table.catalogEntry.loadNavigatorMetaForChildren().done(() => {
-      this.catalogEntry.getComment().done(this.comment);
+    this.table.catalogEntry.loadNavigatorMetaForChildren().finally(() => {
+      this.catalogEntry.getComment().then(this.comment);
     });
   }
 

+ 16 - 15
desktop/core/src/desktop/js/apps/tableBrowser/metastoreDatabase.js

@@ -56,12 +56,12 @@ class MetastoreDatabase {
     this.comment = ko.observable();
 
     this.comment.subscribe(newValue => {
-      this.catalogEntry.getComment().done(comment => {
+      this.catalogEntry.getComment().then(comment => {
         if (comment !== newValue) {
           this.catalogEntry
             .setComment(newValue)
-            .done(this.comment)
-            .fail(() => {
+            .then(this.comment)
+            .catch(() => {
               this.comment(comment);
             });
         }
@@ -110,18 +110,18 @@ class MetastoreDatabase {
       this.loadingComment(true);
       this.catalogEntry
         .getNavigatorMeta()
-        .done(this.navigatorMeta)
-        .always(() => {
+        .then(this.navigatorMeta)
+        .finally(() => {
           this.loadingComment(false);
         });
     }
 
-    this.catalogEntry.getComment().done(this.comment);
+    this.catalogEntry.getComment().then(this.comment);
 
     this.loadingTables(true);
     this.catalogEntry
       .getChildren()
-      .done(tableEntries => {
+      .then(tableEntries => {
         this.tables(
           tableEntries.map(
             tableEntry =>
@@ -137,12 +137,12 @@ class MetastoreDatabase {
           this.loadingTableComments(true);
           this.catalogEntry
             .loadNavigatorMetaForChildren()
-            .done(() => {
+            .then(() => {
               this.tables().forEach(table => {
                 table.navigatorMeta(table.catalogEntry.navigatorMeta);
               });
             })
-            .always(() => {
+            .finally(() => {
               this.loadingTableComments(false);
             });
         }
@@ -150,21 +150,21 @@ class MetastoreDatabase {
           this.loadingTablePopularity(true);
           this.catalogEntry
             .loadOptimizerPopularityForChildren()
-            .done(() => {
+            .then(() => {
               this.tables().forEach(table => {
                 table.optimizerStats(table.catalogEntry.optimizerPopularity);
               });
             })
-            .always(() => {
+            .finally(() => {
               this.loadingTablePopularity(false);
             });
         }
         this.loaded(true);
       })
-      .fail(() => {
+      .catch(() => {
         this.tables([]);
       })
-      .always(() => {
+      .finally(() => {
         this.loadingTables(false);
         if (callback) {
           callback();
@@ -174,8 +174,9 @@ class MetastoreDatabase {
     this.loadingAnalysis(true);
     this.catalogEntry
       .getAnalysis()
-      .done(this.stats)
-      .always(() => {
+      .then(this.stats)
+      .catch(() => {})
+      .finally(() => {
         this.loadingAnalysis(false);
       });
 

+ 3 - 3
desktop/core/src/desktop/js/apps/tableBrowser/metastoreNamespace.js

@@ -78,11 +78,11 @@ class MetastoreNamespace {
         path: [],
         definition: { type: 'source' }
       })
-      .done(entry => {
+      .then(entry => {
         this.catalogEntry(entry);
         entry
           .getChildren()
-          .done(databaseEntries => {
+          .then(databaseEntries => {
             this.databases(
               databaseEntries.map(
                 databaseEntry =>
@@ -95,7 +95,7 @@ class MetastoreNamespace {
             );
             deferred.resolve();
           })
-          .fail(deferred.reject);
+          .catch(deferred.reject);
       });
 
     return this.lastLoadDatabasesPromise;

+ 22 - 21
desktop/core/src/desktop/js/apps/tableBrowser/metastoreTable.js

@@ -110,12 +110,12 @@ class MetastoreTable {
     );
 
     this.comment.subscribe(newValue => {
-      this.catalogEntry.getComment().done(comment => {
+      this.catalogEntry.getComment().then(comment => {
         if (comment !== newValue) {
           this.catalogEntry
             .setComment(newValue)
-            .done(this.comment)
-            .fail(() => {
+            .then(this.comment)
+            .catch(() => {
               this.comment(comment);
             });
         }
@@ -129,10 +129,10 @@ class MetastoreTable {
       this.refreshingTableStats(true);
       this.catalogEntry
         .getAnalysis({ refreshAnalysis: true, silenceErrors: true })
-        .done(() => {
+        .then(() => {
           this.fetchDetails();
         })
-        .fail(data => {
+        .catch(data => {
           this.refreshingTableStats(false);
           $.jHueNotify.error(
             I18n('An error occurred refreshing the table stats. Please try again.')
@@ -146,7 +146,7 @@ class MetastoreTable {
       this.loadingColumns(true);
       this.catalogEntry
         .getChildren()
-        .done(columnEntries => {
+        .then(columnEntries => {
           this.columns(
             columnEntries.map(
               columnEntry =>
@@ -159,7 +159,7 @@ class MetastoreTable {
 
           this.catalogEntry
             .getOptimizerMeta()
-            .done(optimizerMeta => {
+            .then(optimizerMeta => {
               this.optimizerDetails(optimizerMeta);
 
               const topColIndex = {};
@@ -173,14 +173,15 @@ class MetastoreTable {
                 }
               });
             })
-            .always(() => {
+            .catch(() => {})
+            .finally(() => {
               this.loadingQueries(false);
             });
         })
-        .fail(() => {
+        .catch(() => {
           this.columns([]);
         })
-        .always(() => {
+        .finally(() => {
           this.loadingColumns(false);
         });
     };
@@ -189,10 +190,10 @@ class MetastoreTable {
       this.loadingComment(true);
       this.database.catalogEntry
         .loadNavigatorMetaForChildren()
-        .done(() => {
-          this.catalogEntry.getComment().done(this.comment);
+        .then(() => {
+          this.catalogEntry.getComment().then(this.comment);
         })
-        .always(() => {
+        .finally(() => {
           this.loadingComment(false);
         });
 
@@ -202,7 +203,7 @@ class MetastoreTable {
 
       this.catalogEntry
         .getTopJoins({ silenceErrors: true })
-        .done(topJoins => {
+        .then(topJoins => {
           if (topJoins && topJoins.values) {
             const joins = [];
             const ownQidLower = this.catalogEntry.path.join('.').toLowerCase();
@@ -287,14 +288,14 @@ class MetastoreTable {
             this.topJoins(joins);
           }
         })
-        .always(() => {
+        .finally(() => {
           this.loadingTopJoins(false);
         });
 
       this.loadingDetails(true);
       this.catalogEntry
         .getAnalysis()
-        .done(analysis => {
+        .then(analysis => {
           this.tableDetails(analysis);
           this.tableStats(analysis.details.stats);
           this.loaded(true);
@@ -312,17 +313,17 @@ class MetastoreTable {
               if (property.col_name.toLowerCase() === 'view original text:') {
                 apiHelper
                   .formatSql({ statements: property.data_type })
-                  .done(formatResponse => {
+                  .then(formatResponse => {
                     this.viewSql(
                       formatResponse.status === 0
                         ? formatResponse.formatted_statements
                         : property.data_type
                     );
                   })
-                  .fail(() => {
+                  .catch(() => {
                     this.viewSql(property.data_type);
                   })
-                  .always(() => {
+                  .finally(() => {
                     this.loadingViewSql(false);
                   });
                 return true;
@@ -332,12 +333,12 @@ class MetastoreTable {
             this.loadingViewSql(false);
           }
         })
-        .fail(() => {
+        .catch(() => {
           this.partitions.loading(false);
           this.partitions.loaded(true);
           this.loadingViewSql(false);
         })
-        .always(() => {
+        .finally(() => {
           this.refreshingTableStats(false);
           this.loadingDetails(false);
         });

+ 2 - 2
desktop/core/src/desktop/js/apps/tableBrowser/metastoreTablePartitions.js

@@ -105,14 +105,14 @@ class MetastoreTablePartitions {
 
     this.metastoreTable.catalogEntry
       .getPartitions()
-      .done(partitions => {
+      .then(partitions => {
         this.keys(partitions.partition_keys_json);
         this.values(partitions.partition_values_json);
         this.preview.values(this.values().slice(0, 5));
         this.preview.keys(this.keys());
         huePubSub.publish('metastore.loaded.partitions');
       })
-      .always(() => {
+      .finally(() => {
         this.loading(false);
         this.loaded(true);
       });

+ 3 - 3
desktop/core/src/desktop/js/apps/tableBrowser/metastoreTableSamples.js

@@ -45,17 +45,17 @@ class MetastoreTableSamples {
     this.loading(true);
     this.metastoreTable.catalogEntry
       .getSample()
-      .done(sample => {
+      .then(sample => {
         this.rows(sample.data);
         this.headers(sample.meta.map(meta => meta.name));
         this.preview.rows(this.rows().slice(0, 3));
         this.preview.headers(this.headers());
       })
-      .fail(message => {
+      .catch(message => {
         this.errorMessage(message);
         this.hasErrors(true);
       })
-      .always(() => {
+      .finally(() => {
         this.loading(false);
         this.loaded(true);
       });

+ 2 - 1
desktop/core/src/desktop/js/apps/tableBrowser/metastoreViewModel.js

@@ -329,7 +329,8 @@ class MetastoreViewModel {
                           });
                       });
                   }
-                });
+                })
+                .catch(() => {});
             }
           };
 

+ 1826 - 0
desktop/core/src/desktop/js/catalog/DataCatalogEntry.ts

@@ -0,0 +1,1826 @@
+// 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.
+
+import { Cancellable, CancellablePromise } from 'api/cancellablePromise';
+import MultiTableEntry, { TopAggs, TopFilters, TopJoins } from 'catalog/MultiTableEntry';
+import { getOptimizer } from './optimizer/optimizer';
+import * as ko from 'knockout';
+
+import apiHelper from 'api/apiHelper';
+import CancellableJqPromise from 'api/cancellableJqPromise';
+import {
+  applyCancellable,
+  fetchAndSave,
+  FetchOptions,
+  setSilencedErrors
+} from 'catalog/catalogUtils';
+import { Compute, Connector, Namespace } from 'types/config';
+import { hueWindow } from 'types/types';
+import huePubSub from 'utils/huePubSub';
+import I18n from 'utils/i18n';
+import {
+  CatalogGetOptions,
+  DataCatalog,
+  OptimizerPopularity,
+  OptimizerResponse,
+  OptimizerResponsePopularity,
+  TimestampedData
+} from './dataCatalog';
+
+export interface BaseDefinition extends TimestampedData {
+  name?: string;
+  comment?: string;
+  index?: number;
+  type?: string;
+  isMapValue?: boolean;
+  optimizerLoaded?: boolean;
+  partitionKey?: boolean;
+  primaryKey?: boolean;
+  foreignKey?: KeySpecification;
+}
+
+export type Definition = ExtendedColumn | BaseDefinition;
+
+export interface KeySpecification {
+  name: string;
+}
+
+export interface ExtendedColumn extends BaseDefinition, TimestampedData {
+  name: string;
+  type: string;
+}
+
+export interface RootSourceMeta extends TimestampedData {
+  comment?: string | null;
+  databases: string[];
+  notFound?: boolean;
+}
+
+export interface TablesMeta extends TimestampedData {
+  comment?: string | null;
+  index: number;
+  name: string;
+  type: string;
+}
+
+export interface DatabaseSourceMeta extends TimestampedData {
+  comment?: string | null;
+  notFound?: boolean;
+  tables_meta: TablesMeta[];
+}
+
+export interface TableSourceMeta extends TimestampedData {
+  columns: string[];
+  comment?: string | null;
+  extended_columns: ExtendedColumn[];
+  foreign_keys?: KeySpecification[];
+  fields?: string[]; // TODO: On FieldSourceMeta?
+  hdfs_link?: string;
+  is_view?: boolean;
+  notFound?: boolean;
+  partition_keys?: KeySpecification[];
+  primary_keys?: KeySpecification[];
+  support_updates?: boolean;
+}
+
+export interface ComplexDetails {
+  index?: number;
+  isMapValue: boolean;
+  type: string;
+  fields?: {
+    index?: number;
+    type: string;
+    name: string;
+  }[];
+}
+
+export interface FieldSourceMeta extends TimestampedData {
+  comment?: string | null;
+  name: string;
+  notFound?: boolean;
+  sample: FieldSample[][];
+  type: string;
+  key?: ComplexDetails;
+  value?: ComplexDetails;
+  item?: ComplexDetails;
+  fields?: {
+    index?: number;
+    type: string;
+    name: string;
+  }[];
+}
+
+export type SourceMeta = RootSourceMeta | DatabaseSourceMeta | TableSourceMeta | FieldSourceMeta;
+export type FieldSample = string | number | null | undefined;
+
+export interface NavigatorMeta extends TimestampedData {
+  clusteredByColNames: unknown;
+  compressed: boolean;
+  created: string;
+  customProperties: unknown;
+  deleteTime: unknown;
+  deleted: boolean;
+  description: string | null;
+  extractorRunId: string;
+  fileSystemPath: string;
+  firstClassParentId: unknown;
+  identity: string;
+  inputFormat: string;
+  internalType: string;
+  lastAccessed: string | null;
+  lastModified: string | null;
+  lastModifiedBy: string | null;
+  metaClassName: string;
+  name: string | null;
+  originalDescription: string | null;
+  originalName: string;
+  original_name?: string;
+  outputFormat: string;
+  owner: string;
+  packageName: string;
+  parentPath: string;
+  partColNames: unknown;
+  properties: { [key: string]: string };
+  serdeLibName: string;
+  serdeName: string | null;
+  serdeProps: string | null;
+  sortByColNames: string | null;
+  sourceId: string;
+  sourceType: string;
+  tags: unknown;
+  technicalProperties: { [key: string]: string };
+  type: string;
+  userEntity: boolean;
+}
+
+export interface TableAnalysis extends TimestampedData {
+  cols: { comment?: string | null; type: string; name: string };
+  comment?: string | null;
+  details: {
+    properties: { [propertyKey: string]: string };
+    stats: { [statKey: string]: string };
+  };
+  hdfs_link: string;
+  is_view: boolean;
+  message: string;
+  name: string;
+  partition_keys: KeySpecification[];
+  path_location?: string;
+  primary_keys: KeySpecification[];
+  properties: { col_name: string; comment?: string | null; data_type?: string | null }[];
+  stats: { col_name: string; comment?: string | null; data_type?: string | null }[];
+}
+
+export type Analysis = TableAnalysis;
+
+export interface Partitions extends TimestampedData {
+  partition_keys_json: string[];
+  partition_values_json: {
+    browseUrl: string;
+    columns: string[];
+    notebookUrl: string;
+    partitionSpec: string;
+    readUrl: string;
+  };
+}
+
+export interface SampleMeta {
+  comment?: string | null;
+  name: string;
+  type: string;
+}
+
+export interface Sample {
+  data: FieldSample[][];
+  has_more?: boolean;
+  hueTimestamp?: number;
+  isEscaped?: boolean;
+  meta: SampleMeta[];
+  type: string;
+}
+
+export interface OptimizerMeta extends TimestampedData {
+  hueTimestamp?: number;
+}
+
+/**
+ * Helper function to reload the source meta for the given entry
+ */
+const reloadSourceMeta = (
+  entry: DataCatalogEntry,
+  options?: { silenceErrors?: boolean }
+): CancellablePromise<SourceMeta> => {
+  entry.sourceMetaPromise = new CancellablePromise<SourceMeta>(
+    async (resolve, reject, onCancel) => {
+      if (entry.dataCatalog.invalidatePromise) {
+        try {
+          await entry.dataCatalog.invalidatePromise;
+        } catch (err) {}
+      }
+
+      const fetchPromise = fetchAndSave<SourceMeta>(
+        (<(options: FetchOptions) => CancellableJqPromise<SourceMeta>>(
+          (<unknown>apiHelper.fetchSourceMetadata)
+        )).bind(apiHelper),
+        val => {
+          entry.sourceMeta = val;
+        },
+        entry,
+        options
+      );
+
+      onCancel(() => {
+        fetchPromise.cancel();
+        entry.sourceMetaPromise = undefined;
+      });
+
+      fetchPromise.then(resolve).fail(reject);
+    }
+  );
+  return entry.sourceMetaPromise;
+};
+
+/**
+ * Helper function to reload the navigator meta for the given entry
+ */
+const reloadNavigatorMeta = (
+  entry: DataCatalogEntry,
+  apiOptions?: { silenceErrors?: boolean }
+): CancellablePromise<NavigatorMeta> => {
+  if (entry.canHaveNavigatorMetadata()) {
+    entry.navigatorMetaPromise = new CancellablePromise<NavigatorMeta>(
+      (resolve, reject, onCancel) => {
+        const fetchPromise = fetchAndSave(
+          (<(options: FetchOptions) => CancellableJqPromise<NavigatorMeta>>(
+            (<unknown>apiHelper.fetchNavigatorMetadata)
+          )).bind(apiHelper),
+          val => {
+            entry.navigatorMeta = val;
+          },
+          entry,
+          apiOptions
+        );
+
+        onCancel(() => {
+          fetchPromise.cancel();
+          entry.navigatorMetaPromise = undefined;
+        });
+
+        fetchPromise
+          .then(navigatorMeta => {
+            if (!navigatorMeta) {
+              reject();
+            } else {
+              if (entry.commentObservable) {
+                entry.commentObservable(entry.getResolvedComment());
+              }
+              resolve(navigatorMeta);
+            }
+          })
+          .fail(reject);
+      }
+    );
+  } else {
+    entry.navigatorMetaPromise = CancellablePromise.reject();
+  }
+  return entry.navigatorMetaPromise;
+};
+
+/**
+ * Helper function to reload the analysis for the given entry
+ */
+const reloadAnalysis = (
+  entry: DataCatalogEntry,
+  apiOptions?: { silenceErrors?: boolean; refreshAnalysis?: boolean }
+): CancellablePromise<Analysis> => {
+  entry.analysisPromise = new CancellablePromise<Analysis>((resolve, reject, onCancel) => {
+    const apiFunction =
+      apiOptions && apiOptions.refreshAnalysis
+        ? apiHelper.refreshAnalysis.bind(apiHelper)
+        : apiHelper.fetchAnalysis.bind(apiHelper);
+
+    const fetchPromise = fetchAndSave(
+      (<(options: FetchOptions) => CancellableJqPromise<Analysis>>(<unknown>apiFunction)).bind(
+        apiHelper
+      ),
+      val => {
+        entry.analysis = val;
+      },
+      entry,
+      apiOptions
+    );
+
+    onCancel(() => {
+      fetchPromise.cancel();
+      entry.analysis = undefined;
+    });
+
+    fetchPromise.then(resolve).fail(reject);
+  });
+  return entry.analysisPromise;
+};
+
+/**
+ * Helper function to reload the partitions for the given entry
+ */
+const reloadPartitions = (
+  entry: DataCatalogEntry,
+  apiOptions?: { silenceErrors?: boolean }
+): CancellablePromise<Partitions> => {
+  entry.partitionsPromise = new CancellablePromise<Partitions>((resolve, reject, onCancel) => {
+    const fetchPromise = fetchAndSave(
+      (<(options: FetchOptions) => CancellableJqPromise<Partitions>>(
+        (<unknown>apiHelper.fetchPartitions)
+      )).bind(apiHelper),
+      val => {
+        entry.partitions = val;
+      },
+      entry,
+      apiOptions
+    );
+
+    onCancel(() => {
+      fetchPromise.cancel();
+      entry.partitionsPromise = undefined;
+    });
+
+    fetchPromise.then(resolve).fail(reject);
+  });
+  return entry.partitionsPromise;
+};
+
+/**
+ * Helper function to reload the sample for the given entry
+ */
+const reloadSample = (
+  entry: DataCatalogEntry,
+  apiOptions?: { silenceErrors?: boolean }
+): CancellablePromise<Sample> => {
+  entry.samplePromise = new CancellablePromise<Sample>((resolve, reject, onCancel) => {
+    const fetchPromise = fetchAndSave(
+      (<(options: FetchOptions) => CancellableJqPromise<Sample>>(
+        (<unknown>apiHelper.fetchSample)
+      )).bind(apiHelper),
+      val => {
+        entry.sample = val;
+      },
+      entry,
+      apiOptions
+    );
+
+    onCancel(() => {
+      fetchPromise.cancel();
+      entry.samplePromise = undefined;
+    });
+
+    fetchPromise.then(resolve).fail(reject);
+  });
+  return entry.samplePromise;
+};
+
+/**
+ * Helper function to reload the nav opt metadata for the given entry
+ */
+const reloadOptimizerMeta = (
+  entry: DataCatalogEntry,
+  apiOptions?: { silenceErrors?: boolean }
+): CancellablePromise<OptimizerMeta> => {
+  if (entry.dataCatalog.canHaveOptimizerMeta()) {
+    const optimizer = getOptimizer(entry.dataCatalog.connector);
+
+    entry.optimizerMetaPromise = new CancellablePromise<OptimizerMeta>(
+      (resolve, reject, onCancel) => {
+        const fetchPromise = fetchAndSave(
+          (<(options: FetchOptions) => CancellableJqPromise<OptimizerMeta>>(
+            (<unknown>optimizer.fetchOptimizerMeta)
+          )).bind(apiHelper),
+          val => {
+            entry.optimizerMeta = val;
+          },
+          entry,
+          apiOptions
+        );
+
+        onCancel(() => {
+          fetchPromise.cancel();
+          entry.optimizerMetaPromise = undefined;
+        });
+
+        fetchPromise.then(resolve).fail(reject);
+      }
+    );
+  } else {
+    entry.optimizerMetaPromise = CancellablePromise.reject();
+  }
+  return entry.optimizerMetaPromise;
+};
+
+/**
+ * Helper function to get the multi table catalog version of a catalog entry
+ */
+const getMultiTableEntry = async (catalogEntry: DataCatalogEntry): Promise<MultiTableEntry> => {
+  if (!catalogEntry.isTableOrView()) {
+    return Promise.reject();
+  }
+  return catalogEntry.dataCatalog.getMultiTableEntry({
+    namespace: catalogEntry.namespace,
+    compute: catalogEntry.compute,
+    paths: [catalogEntry.path]
+  });
+};
+
+export default class DataCatalogEntry {
+  compute: Compute;
+  dataCatalog: DataCatalog;
+  definition?: Definition;
+  isTemporary?: boolean;
+  name: string;
+  namespace: Namespace;
+  path: string[];
+  saveTimeout = -1;
+  commentObservable?: KnockoutObservable<string | undefined>;
+
+  analysis?: Analysis;
+  analysisPromise?: CancellablePromise<Analysis>;
+  childrenPromise?: CancellablePromise<DataCatalogEntry[]>;
+  navigatorMeta?: NavigatorMeta;
+  navigatorMetaForChildrenPromise?: CancellablePromise<DataCatalogEntry[]>;
+  navigatorMetaPromise?: CancellablePromise<NavigatorMeta>;
+  optimizerMeta?: OptimizerMeta;
+  optimizerMetaPromise?: CancellablePromise<OptimizerMeta>;
+  optimizerPopularity?: OptimizerPopularity;
+  optimizerPopularityForChildrenPromise?: CancellablePromise<DataCatalogEntry[]>;
+  partitions?: Partitions;
+  partitionsPromise?: CancellablePromise<Partitions>;
+  sample?: Sample;
+  samplePromise?: CancellablePromise<Sample>;
+  sourceMeta?: SourceMeta;
+  sourceMetaPromise?: CancellablePromise<SourceMeta>;
+
+  constructor(options: {
+    compute: Compute;
+    dataCatalog: DataCatalog;
+    definition?: Definition;
+    isTemporary?: boolean;
+    namespace: Namespace;
+    path?: string | string[];
+  }) {
+    if (!options.dataCatalog.connector) {
+      throw new Error('DataCatalogEntry created without connector');
+    }
+
+    this.namespace = options.namespace;
+    this.compute = options.compute;
+    this.dataCatalog = options.dataCatalog;
+
+    this.path = typeof options.path === 'string' ? options.path.split('.') : options.path || [];
+    this.name = this.path.length ? this.path[this.path.length - 1] : this.getConnector().id;
+    this.isTemporary = options.isTemporary;
+
+    if (options.definition) {
+      this.definition = options.definition;
+    } else if (this.path.length === 0) {
+      this.definition = { type: 'source' };
+    } else if (this.path.length === 1) {
+      this.definition = { type: 'database' };
+    } else if (this.path.length === 2) {
+      this.definition = { type: 'table' };
+    }
+
+    this.reset();
+  }
+
+  /**
+   * Resets the entry to an empty state, it might still have some details cached
+   */
+  reset(): void {
+    this.saveTimeout = -1;
+    this.analysis = undefined;
+    this.analysisPromise = undefined;
+    this.childrenPromise = undefined;
+    this.navigatorMeta = undefined;
+    this.navigatorMetaForChildrenPromise = undefined;
+    this.navigatorMetaPromise = undefined;
+    this.optimizerMeta = undefined;
+    this.optimizerMetaPromise = undefined;
+    this.optimizerPopularity = undefined;
+    this.optimizerPopularityForChildrenPromise = undefined;
+    this.partitions = undefined;
+    this.partitionsPromise = undefined;
+    this.sample = undefined;
+    this.samplePromise = undefined;
+    this.sourceMeta = undefined;
+    this.sourceMetaPromise = undefined;
+
+    if (this.path.length) {
+      this.dataCatalog
+        .getKnownEntry({
+          namespace: this.namespace,
+          compute: this.compute,
+          path: this.path.slice(0, this.path.length - 1)
+        })
+        .then(parent => {
+          if (parent) {
+            parent.navigatorMetaForChildrenPromise = undefined;
+            parent.optimizerPopularityForChildrenPromise = undefined;
+          }
+        });
+    }
+  }
+
+  /**
+   * Resets the entry and clears the cache
+   */
+  async clearCache(options: {
+    cascade?: boolean;
+    silenceErrors?: boolean;
+    targetChild?: string;
+  }): Promise<void> {
+    if (!options) {
+      options = {};
+    }
+
+    if (this.definition && this.definition.optimizerLoaded) {
+      delete this.definition.optimizerLoaded;
+    }
+
+    this.reset();
+
+    try {
+      if (options.cascade) {
+        await this.dataCatalog.clearStorageCascade(this.namespace, this.compute, this.path);
+      } else {
+        await this.save();
+      }
+    } catch (err) {}
+
+    huePubSub.publish('data.catalog.entry.refreshed', {
+      entry: this,
+      cascade: !!options.cascade
+    });
+  }
+
+  /**
+   * Save the entry to cache
+   */
+  async save(): Promise<void> {
+    window.clearTimeout(this.saveTimeout);
+    try {
+      await this.dataCatalog.persistCatalogEntry(this);
+    } catch (err) {}
+  }
+
+  /**
+   * Save the entry at a later point of time
+   */
+  saveLater(): void {
+    if (((<hueWindow>window).CACHEABLE_TTL?.default || 0) > 0) {
+      window.clearTimeout(this.saveTimeout);
+      this.saveTimeout = window.setTimeout(async () => {
+        await this.save();
+      }, 1000);
+    }
+  }
+
+  /**
+   * Gets the parent entry, rejected if there's no parent.
+   */
+  getParent(): Promise<DataCatalogEntry> {
+    if (!this.path.length) {
+      return Promise.reject();
+    }
+
+    return this.dataCatalog.getEntry({
+      namespace: this.namespace,
+      compute: this.compute,
+      path: this.path.slice(0, this.path.length - 1)
+    });
+  }
+
+  /**
+   * Get the children of the catalog entry, columns for a table entry etc.
+   */
+  getChildren(options?: CatalogGetOptions): CancellablePromise<DataCatalogEntry[]> {
+    if (this.childrenPromise && DataCatalog.cacheEnabled() && (!options || !options.refreshCache)) {
+      return applyCancellable(this.childrenPromise, options);
+    }
+
+    if (!this.childrenPromise && options && options.cachedOnly) {
+      return CancellablePromise.reject(false);
+    }
+
+    this.childrenPromise = new CancellablePromise<DataCatalogEntry[]>(
+      async (resolve, reject, onCancel) => {
+        let sourceMeta: SourceMeta | undefined;
+        try {
+          const sourceMetaPromise = this.getSourceMeta(options);
+          onCancel(() => {
+            sourceMetaPromise.cancel();
+          });
+          sourceMeta = await sourceMetaPromise;
+        } catch (err) {}
+
+        if (!sourceMeta || sourceMeta.notFound) {
+          reject('No source meta found');
+          return;
+        }
+
+        const partitionKeys: { [key: string]: boolean } = {};
+        const tableSourceMeta = <TableSourceMeta>sourceMeta;
+        if (tableSourceMeta.partition_keys) {
+          tableSourceMeta.partition_keys.forEach(partitionKey => {
+            partitionKeys[partitionKey.name] = true;
+          });
+        }
+        const primaryKeys: { [key: string]: boolean } = {};
+        if (tableSourceMeta.primary_keys) {
+          tableSourceMeta.primary_keys.forEach(primaryKey => {
+            primaryKeys[primaryKey.name] = true;
+          });
+        }
+        const foreignKeys: { [key: string]: KeySpecification } = {};
+        if (tableSourceMeta.foreign_keys) {
+          tableSourceMeta.foreign_keys.forEach(foreignKey => {
+            foreignKeys[foreignKey.name] = foreignKey;
+          });
+        }
+
+        const entities: string[] | ExtendedColumn[] =
+          (<RootSourceMeta>sourceMeta).databases ||
+          (<DatabaseSourceMeta>sourceMeta).tables_meta ||
+          (<TableSourceMeta>sourceMeta).extended_columns ||
+          (<FieldSourceMeta>sourceMeta).fields ||
+          (<TableSourceMeta>sourceMeta).columns ||
+          [];
+
+        const promises: Promise<DataCatalogEntry>[] = [];
+        let index = 0;
+        entities.forEach((entity: string | ExtendedColumn) => {
+          if (!(<RootSourceMeta>sourceMeta).databases || entity !== '_impala_builtins') {
+            const name = (<ExtendedColumn>entity).name || <string>entity;
+            const promise = this.dataCatalog.getEntry({
+              namespace: this.namespace,
+              compute: this.compute,
+              path: [...this.path, name]
+            });
+
+            promise.then(catalogEntry => {
+              if (
+                !catalogEntry.definition ||
+                typeof catalogEntry.definition.index === 'undefined'
+              ) {
+                const definition: BaseDefinition =
+                  typeof entity === 'object' ? entity : { name: entity };
+                if (!definition.type) {
+                  if (this.path.length === 0) {
+                    definition.type = 'database';
+                  } else if (this.path.length === 1) {
+                    definition.type = 'table';
+                  } else if (this.path.length === 2) {
+                    definition.type = 'column';
+                  }
+                }
+
+                if ((<TableSourceMeta>sourceMeta).partition_keys) {
+                  definition.partitionKey = partitionKeys[name];
+                }
+                if ((<TableSourceMeta>sourceMeta).primary_keys) {
+                  definition.primaryKey = primaryKeys[name];
+                }
+                if ((<TableSourceMeta>sourceMeta).foreign_keys) {
+                  definition.foreignKey = foreignKeys[name];
+                }
+                definition.index = index++;
+                catalogEntry.definition = definition;
+                catalogEntry.saveLater();
+              }
+            });
+            promises.push(promise);
+          }
+        });
+
+        // TODO: Move to connector attributes
+        if (
+          sourceMeta &&
+          (this.getDialect() === 'impala' || this.getDialect() === 'hive') &&
+          this.isComplex()
+        ) {
+          const fieldSourceMeta = <FieldSourceMeta>sourceMeta;
+          const complexAttributes: (keyof Pick<FieldSourceMeta, 'key' | 'value' | 'item'>)[] =
+            fieldSourceMeta.type === 'map' ? ['key', 'value'] : ['item'];
+          complexAttributes.forEach(path => {
+            const definition = fieldSourceMeta[path];
+            if (definition) {
+              const promise = this.dataCatalog.getEntry({
+                namespace: this.namespace,
+                compute: this.compute,
+                path: [...this.path, path]
+              });
+              promise.then(catalogEntry => {
+                if (
+                  !catalogEntry.definition ||
+                  typeof catalogEntry.definition.index === 'undefined'
+                ) {
+                  definition.index = index++;
+                  definition.isMapValue = path === 'value';
+                  catalogEntry.definition = definition;
+                  catalogEntry.saveLater();
+                }
+              });
+              promises.push(promise);
+            }
+          });
+        }
+
+        Promise.all(promises).then(resolve).catch(reject);
+      }
+    );
+
+    return applyCancellable(this.childrenPromise, options);
+  }
+
+  /**
+   * Loads navigator metadata for children, only applicable to databases and tables
+   */
+  loadNavigatorMetaForChildren(
+    options?: Omit<CatalogGetOptions, 'cachedOnly'>
+  ): CancellablePromise<DataCatalogEntry[]> {
+    options = setSilencedErrors(options);
+
+    if (!this.canHaveNavigatorMetadata() || this.isField()) {
+      return CancellablePromise.resolve([]);
+    }
+
+    if (
+      this.navigatorMetaForChildrenPromise &&
+      DataCatalog.cacheEnabled() &&
+      (!options || !options.refreshCache)
+    ) {
+      return applyCancellable(this.navigatorMetaForChildrenPromise, options);
+    }
+
+    this.navigatorMetaForChildrenPromise = new CancellablePromise<DataCatalogEntry[]>(
+      async (resolve, reject, onCancel) => {
+        const cancellablePromises: Cancellable[] = [];
+        onCancel(() => {
+          cancellablePromises.forEach(cancellable => {
+            cancellable.cancel();
+          });
+        });
+
+        try {
+          const childPromise = this.getChildren(options);
+          cancellablePromises.push(childPromise);
+
+          const children = await childPromise;
+
+          const someHaveNavMeta = children.some(childEntry => childEntry.navigatorMeta);
+
+          if (
+            someHaveNavMeta &&
+            DataCatalog.cacheEnabled() &&
+            (!options || !options.refreshCache)
+          ) {
+            resolve(children);
+            return;
+          }
+
+          // TODO: Add sourceType to nav search query
+          const query = this.path.length
+            ? `parentPath:"/${this.path.join('/')}" AND type:(table view field)`
+            : 'type:database';
+
+          const rejectUnknown = () => {
+            children.forEach(childEntry => {
+              if (!childEntry.navigatorMeta) {
+                childEntry.navigatorMeta = undefined;
+                childEntry.navigatorMetaPromise = undefined;
+              }
+            });
+          };
+
+          const searchPromise = apiHelper.searchEntities({
+            query: query,
+            rawQuery: true,
+            limit: children.length,
+            silenceErrors: options && options.silenceErrors
+          });
+
+          cancellablePromises.push(searchPromise);
+
+          searchPromise
+            .done((result: { entities: NavigatorMeta[] }) => {
+              if (result && result.entities) {
+                const childEntryIndex: { [name: string]: DataCatalogEntry } = {};
+                children.forEach(childEntry => {
+                  childEntryIndex[childEntry.name.toLowerCase()] = childEntry;
+                });
+
+                result.entities.forEach(entity => {
+                  const matchingChildEntry =
+                    childEntryIndex[(entity.original_name || entity.originalName).toLowerCase()];
+                  if (matchingChildEntry) {
+                    matchingChildEntry.navigatorMeta = entity;
+                    entity.hueTimestamp = Date.now();
+                    matchingChildEntry.navigatorMetaPromise = CancellablePromise.resolve(
+                      matchingChildEntry.navigatorMeta
+                    );
+                    if (entity && matchingChildEntry.commentObservable) {
+                      matchingChildEntry.commentObservable(matchingChildEntry.getResolvedComment());
+                    }
+                    matchingChildEntry.saveLater();
+                  }
+                });
+              }
+            })
+            .always(() => {
+              rejectUnknown();
+              resolve(children);
+            });
+        } catch (err) {
+          resolve([]);
+          return;
+        }
+      }
+    );
+
+    return applyCancellable(this.navigatorMetaForChildrenPromise, options);
+  }
+
+  /**
+   * Helper function used when loading navopt metdata for children
+   */
+  applyOptimizerResponseToChildren(
+    response: OptimizerResponse,
+    options?: { silenceErrors?: boolean }
+  ): CancellablePromise<DataCatalogEntry[]> {
+    if (!this.definition) {
+      this.definition = {};
+    }
+    this.definition.optimizerLoaded = true;
+    this.saveLater();
+
+    return new CancellablePromise<DataCatalogEntry[]>(async (resolve, reject, onCancel) => {
+      const childPromise = this.getChildren(options);
+      onCancel(() => {
+        childPromise.cancel();
+      });
+
+      try {
+        const childEntries = await childPromise;
+
+        const entriesByName: { [name: string]: DataCatalogEntry } = {};
+        childEntries.forEach(childEntry => {
+          entriesByName[childEntry.name.toLowerCase()] = childEntry;
+        });
+        const updatedIndex: { [path: string]: DataCatalogEntry } = {};
+        if (this.isDatabase() && response.top_tables) {
+          response.top_tables.forEach(topTable => {
+            if (!topTable.name) {
+              return;
+            }
+            const matchingChild = entriesByName[topTable.name.toLowerCase()];
+            if (matchingChild) {
+              matchingChild.optimizerPopularity = topTable;
+              matchingChild.saveLater();
+              updatedIndex[matchingChild.getQualifiedPath()] = matchingChild;
+            }
+          });
+        } else if (this.isTableOrView() && response.values) {
+          const addOptimizerPopularity = (
+            columns: OptimizerResponsePopularity[] | undefined,
+            type: keyof Pick<
+              OptimizerPopularity,
+              'filterColumn' | 'groupByColumn' | 'joinColumn' | 'orderByColumn' | 'selectColumn'
+            >
+          ) => {
+            if (columns) {
+              columns.forEach(column => {
+                if (!column.columnName) {
+                  return;
+                }
+                const matchingChild = entriesByName[column.columnName.toLowerCase()];
+                if (matchingChild) {
+                  if (!matchingChild.optimizerPopularity) {
+                    matchingChild.optimizerPopularity = { column_count: 0, columnCount: 0 };
+                  }
+                  matchingChild.optimizerPopularity[type] = column;
+                  matchingChild.saveLater();
+                  updatedIndex[matchingChild.getQualifiedPath()] = matchingChild;
+                }
+              });
+            }
+          };
+
+          addOptimizerPopularity(response.values.filterColumns, 'filterColumn');
+          addOptimizerPopularity(response.values.groupbyColumns, 'groupByColumn');
+          addOptimizerPopularity(response.values.joinColumns, 'joinColumn');
+          addOptimizerPopularity(response.values.orderbyColumns, 'orderByColumn');
+          addOptimizerPopularity(response.values.selectColumns, 'selectColumn');
+        }
+        const popularEntries: DataCatalogEntry[] = [];
+        Object.keys(updatedIndex).forEach(path => {
+          popularEntries.push(updatedIndex[path]);
+        });
+        resolve(popularEntries);
+      } catch (err) {
+        reject(err);
+      }
+    });
+  }
+
+  /**
+   * Loads nav opt popularity for the children of this entry.
+   */
+  loadOptimizerPopularityForChildren(
+    options?: CatalogGetOptions
+  ): CancellablePromise<DataCatalogEntry[]> {
+    options = setSilencedErrors(options);
+
+    if (!this.dataCatalog.canHaveOptimizerMeta()) {
+      return CancellablePromise.reject();
+    }
+
+    if (
+      this.optimizerPopularityForChildrenPromise &&
+      DataCatalog.cacheEnabled() &&
+      (!options || !options.refreshCache)
+    ) {
+      return applyCancellable(this.optimizerPopularityForChildrenPromise, options);
+    }
+
+    if (
+      this.definition &&
+      this.definition.optimizerLoaded &&
+      DataCatalog.cacheEnabled() &&
+      (!options || !options.refreshCache)
+    ) {
+      this.optimizerPopularityForChildrenPromise = new CancellablePromise<DataCatalogEntry[]>(
+        async (resolve, reject, onCancel) => {
+          const childPromise = this.getChildren(options);
+          onCancel(() => {
+            childPromise.cancel();
+          });
+          try {
+            const children = await childPromise;
+            resolve(children.filter(child => child.optimizerPopularity));
+          } catch (err) {
+            reject(err);
+          }
+        }
+      );
+    } else if (this.isDatabase() || this.isTableOrView()) {
+      this.optimizerPopularityForChildrenPromise = new CancellablePromise<DataCatalogEntry[]>(
+        (resolve, reject, onCancel) => {
+          const cancellablePromises: Cancellable[] = [];
+          onCancel(() => {
+            cancellablePromises.forEach(cancellable => cancellable.cancel());
+          });
+
+          const popularityPromise = getOptimizer(this.dataCatalog.connector).fetchPopularity({
+            silenceErrors: options && options.silenceErrors,
+            refreshCache: options && options.refreshCache,
+            paths: [this.path]
+          });
+          cancellablePromises.push(popularityPromise);
+
+          popularityPromise.done((data: OptimizerResponse) => {
+            const applyPromise = this.applyOptimizerResponseToChildren(data, options);
+            cancellablePromises.push(applyPromise);
+            applyPromise.then(resolve).catch(reject);
+          });
+        }
+      );
+    } else {
+      this.optimizerPopularityForChildrenPromise = CancellablePromise.resolve([]);
+    }
+
+    return applyCancellable(this.optimizerPopularityForChildrenPromise);
+  }
+
+  /**
+   * Returns true if the catalog entry can have navigator metadata
+   */
+  canHaveNavigatorMetadata(): boolean {
+    if (!(<hueWindow>window).HAS_CATALOG) {
+      return false;
+    }
+    // TODO: Move to connector attributes
+    return (
+      (this.getDialect() === 'hive' || this.getDialect() === 'impala') &&
+      (this.isDatabase() || this.isTableOrView() || this.isColumn())
+    );
+  }
+
+  /**
+   * Returns the currently known comment without loading any additional metadata
+   */
+  getResolvedComment(): string {
+    // TODO: Move to connector attributes
+    if (this.navigatorMeta && (this.getDialect() === 'hive' || this.getDialect() === 'impala')) {
+      if (this.navigatorMeta.description) {
+        return this.navigatorMeta.description;
+      }
+      if (this.navigatorMeta.originalDescription) {
+        return this.navigatorMeta.originalDescription;
+      }
+    }
+    if (this.definition && this.definition.comment) {
+      return this.definition.comment;
+    }
+    return (this.sourceMeta && this.sourceMeta.comment) || '';
+  }
+
+  /**
+   * This can be used to get an observable for the comment which will be updated once a comment has been
+   * resolved.
+   */
+  getCommentObservable(): KnockoutObservable<string | undefined> {
+    if (!this.commentObservable) {
+      this.commentObservable = ko.observable(this.getResolvedComment());
+    }
+    return this.commentObservable;
+  }
+
+  /**
+   * Checks whether the comment is known and has been loaded from the proper source
+   */
+  hasResolvedComment(): boolean {
+    if (this.canHaveNavigatorMetadata()) {
+      return typeof this.navigatorMeta !== 'undefined';
+    }
+    return typeof this.sourceMeta !== 'undefined';
+  }
+
+  /**
+   * Gets the comment for this entry, fetching it if necessary from the proper source.
+   */
+  getComment(options?: CatalogGetOptions): CancellablePromise<string> {
+    const promise = new CancellablePromise<string>(async (resolve, reject, onCancel) => {
+      const cancellablePromises: Cancellable[] = [];
+      onCancel(() => {
+        cancellablePromises.forEach(cancellable => cancellable.cancel());
+      });
+
+      if (this.canHaveNavigatorMetadata()) {
+        const navigatorMetaPromise = this.getNavigatorMeta(options);
+        cancellablePromises.push(navigatorMetaPromise);
+        try {
+          const navigatorMeta = await navigatorMetaPromise;
+          if (navigatorMeta) {
+            resolve(navigatorMeta.description || navigatorMeta.originalDescription || '');
+            return;
+          }
+        } catch (err) {}
+      }
+
+      if (this.sourceMeta) {
+        resolve(this.sourceMeta.comment || '');
+      } else if (this.definition && this.definition.comment) {
+        resolve(this.definition.comment);
+      } else {
+        const sourceMetaPromise = this.getSourceMeta(options);
+        cancellablePromises.push(sourceMetaPromise);
+        try {
+          const sourceMeta = await sourceMetaPromise;
+          resolve((sourceMeta && sourceMeta.comment) || '');
+        } catch (err) {
+          reject(err);
+        }
+      }
+    });
+
+    return applyCancellable(promise);
+  }
+
+  /**
+   * Updates custom navigator metadata for the catalog entry
+   */
+  async updateNavigatorCustomMetadata(
+    modifiedCustomMetadata?: { [key: string]: string },
+    deletedCustomMetadataKeys?: string[],
+    apiOptions?: Omit<CatalogGetOptions, 'cachedOnly' | 'cancellable'>
+  ): Promise<NavigatorMeta> {
+    if (!this.canHaveNavigatorMetadata()) {
+      return Promise.reject();
+    }
+
+    const navigatorMeta = await this.getNavigatorMeta(apiOptions);
+
+    if (!navigatorMeta) {
+      throw new Error('Could not load navigator metadata.');
+    }
+
+    return new Promise<NavigatorMeta>((resolve, reject) => {
+      apiHelper
+        .updateNavigatorProperties({
+          identity: navigatorMeta.identity,
+          modifiedCustomMetadata: modifiedCustomMetadata,
+          deletedCustomMetadataKeys: deletedCustomMetadataKeys
+        })
+        .done(entity => {
+          if (entity) {
+            this.navigatorMeta = entity;
+            this.navigatorMetaPromise = CancellablePromise.resolve(entity);
+            this.saveLater();
+            resolve(entity);
+          } else {
+            reject();
+          }
+        });
+    });
+  }
+
+  /**
+   * Sets the comment in the proper source
+   *
+   * @param {string} comment
+   * @param {Object} [apiOptions]
+   * @param {boolean} [apiOptions.silenceErrors]
+   * @param {boolean} [apiOptions.refreshCache]
+   *
+   * @return {Promise}
+   */
+  async setComment(
+    comment: string,
+    apiOptions?: Omit<CatalogGetOptions, 'cachedOnly' | 'cancellable'>
+  ): Promise<string> {
+    if (this.canHaveNavigatorMetadata()) {
+      const navigatorMeta = await this.getNavigatorMeta(apiOptions);
+      if (!navigatorMeta) {
+        throw new Error('Could not load navigator metadata.');
+      }
+
+      return new Promise<string>((resolve, reject) => {
+        apiHelper
+          .updateNavigatorProperties({
+            identity: navigatorMeta.identity,
+            properties: {
+              description: comment
+            }
+          })
+          .done(async entity => {
+            if (entity) {
+              this.navigatorMeta = entity;
+              this.navigatorMetaPromise = CancellablePromise.resolve(entity);
+              this.saveLater();
+            }
+            this.getComment(apiOptions)
+              .then(comment => {
+                if (this.commentObservable) {
+                  this.commentObservable(comment);
+                }
+                resolve(comment);
+              })
+              .catch(reject);
+          })
+          .fail(reject);
+      });
+    }
+
+    return new Promise((resolve, reject) => {
+      apiHelper
+        .updateSourceMetadata({
+          sourceType: this.getConnector().id,
+          path: this.path,
+          properties: {
+            comment: comment
+          }
+        })
+        .done(async () => {
+          try {
+            await reloadSourceMeta(this, {
+              silenceErrors: apiOptions && apiOptions.silenceErrors
+            });
+            const comment = await this.getComment(apiOptions);
+            if (this.commentObservable) {
+              this.commentObservable(comment);
+            }
+            resolve(comment);
+          } catch (err) {
+            reject(err);
+          }
+        })
+        .fail(reject);
+    });
+  }
+
+  /**
+   * Adds a list of tags and updates the navigator metadata of the entry
+   */
+  async addNavigatorTags(
+    tags: string[],
+    apiOptions?: Omit<CatalogGetOptions, 'cachedOnly' | 'cancellable'>
+  ): Promise<NavigatorMeta> {
+    if (!this.canHaveNavigatorMetadata()) {
+      return Promise.reject();
+    }
+
+    const navigatorMeta = await this.getNavigatorMeta(apiOptions);
+
+    return new Promise((resolve, reject) => {
+      apiHelper
+        .addNavTags(navigatorMeta.identity, tags)
+        .done(entity => {
+          if (entity) {
+            this.navigatorMeta = entity;
+            this.navigatorMetaPromise = CancellablePromise.resolve(entity);
+            this.saveLater();
+            resolve(entity);
+          } else {
+            reject();
+          }
+        })
+        .fail(reject);
+    });
+  }
+
+  /**
+   * Removes a list of tags and updates the navigator metadata of the entry
+   */
+  async deleteNavigatorTags(
+    tags: string[],
+    apiOptions?: Omit<CatalogGetOptions, 'cachedOnly' | 'cancellable'>
+  ): Promise<NavigatorMeta> {
+    if (!this.canHaveNavigatorMetadata()) {
+      return Promise.reject();
+    }
+
+    const navigatorMeta = await this.getNavigatorMeta(apiOptions);
+
+    return new Promise((resolve, reject) => {
+      apiHelper
+        .deleteNavTags(navigatorMeta.identity, tags)
+        .done(entity => {
+          if (entity) {
+            this.navigatorMeta = entity;
+            this.navigatorMetaPromise = CancellablePromise.resolve(entity);
+            this.saveLater();
+            resolve(entity);
+          } else {
+            reject();
+          }
+        })
+        .fail(reject);
+    });
+  }
+
+  /**
+   * Checks if the entry can have children or not without fetching additional metadata.
+   */
+  hasPossibleChildren(): boolean {
+    return (
+      this.path.length < 3 ||
+      (!this.definition && !this.sourceMeta) ||
+      (!!this.sourceMeta &&
+        /^(?:struct|array|map)/i.test((<FieldSourceMeta>this.sourceMeta).type)) ||
+      (!!this.definition &&
+        !!this.definition.type &&
+        /^(?:struct|array|map)/i.test(this.definition.type))
+    );
+  }
+
+  /**
+   * Returns the index representing the order in which the backend returned this entry.
+   */
+  getIndex(): number {
+    return (this.definition && this.definition.index) || 0;
+  }
+
+  /**
+   * Returns the dialect of this entry.
+   */
+  getDialect(): string {
+    return this.getConnector().dialect || this.getConnector().id; // .id for editor v1
+  }
+
+  /**
+   * Returns the connector for this entry
+   */
+  getConnector(): Connector {
+    return this.dataCatalog.connector;
+  }
+
+  /**
+   * Returns true if the entry represents a data source.
+   *
+   * @return {boolean}
+   */
+  isSource(): boolean {
+    return this.path.length === 0;
+  }
+
+  /**
+   * Returns true if the entry is a database.
+   */
+  isDatabase(): boolean {
+    return this.path.length === 1;
+  }
+
+  /**
+   * Returns true if the entry is a table or a view.
+   *
+   * @return {boolean}
+   */
+  isTableOrView(): boolean {
+    return this.path.length === 2;
+  }
+
+  /**
+   * Returns the default title used for the entry, the qualified path with type for fields. Optionally include
+   * the comment after, if already resolved.
+   */
+  getTitle(includeComment?: boolean): string {
+    let title = this.getQualifiedPath();
+    if (this.isField()) {
+      const type = this.getType();
+      if (type) {
+        title += ' (' + type + ')';
+      }
+    } else if (
+      this.definition &&
+      this.definition.type &&
+      this.definition.type.toLowerCase() === 'materialized_view'
+    ) {
+      title += ' (' + I18n('Materialized') + ')';
+    }
+    if (includeComment && this.hasResolvedComment() && this.getResolvedComment()) {
+      title += ' - ' + this.getResolvedComment();
+    }
+    return title;
+  }
+
+  /**
+   * Returns the fully qualified path for this entry.
+   */
+  getQualifiedPath(): string {
+    return this.path.join('.');
+  }
+
+  /**
+   * Returns the display name for the entry, name or qualified path plus type for fields
+   */
+  getDisplayName(qualified?: boolean): string {
+    const displayName = qualified ? this.getQualifiedPath() : this.name;
+    if (this.isField()) {
+      const type = this.getType();
+      if (type) {
+        return `${displayName}(${type})`;
+      }
+    }
+    return displayName;
+  }
+
+  /**
+   * Returns true for columns that are a primary key. Note that the definition has to come from a parent entry, i.e.
+   * getChildren().
+   */
+  isPrimaryKey(): boolean {
+    return !!(this.isColumn() && this.definition && (<ExtendedColumn>this.definition).primaryKey);
+  }
+
+  /**
+   * Returns true if the entry is a partition key. Note that the definition has to come from a parent entry, i.e.
+   * getChildren().
+   */
+  isPartitionKey(): boolean {
+    return !!(this.definition && (<ExtendedColumn>this.definition).partitionKey);
+  }
+
+  /**
+   * Returns true if the entry is a foreign key. Note that the definition has to come from a parent entry, i.e.
+   * getChildren().
+   */
+  isForeignKey(): boolean {
+    return !!this.definition && !!(<ExtendedColumn>this.definition).foreignKey;
+  }
+
+  /**
+   * Returns true if the entry is either a partition or primary key. Note that the definition has to come from a parent entry, i.e.
+   * getChildren().
+   */
+  isKey(): boolean {
+    return this.isPartitionKey() || this.isPrimaryKey() || this.isForeignKey();
+  }
+
+  /**
+   * Returns true if the entry is a table. It will be accurate once the source meta has been loaded.
+   */
+  isTable(): boolean {
+    if (this.path.length === 2) {
+      if (
+        this.analysis &&
+        this.analysis.details &&
+        this.analysis.details.properties &&
+        this.analysis.details.properties.table_type === 'VIRTUAL_VIEW'
+      ) {
+        return false;
+      }
+      if (this.sourceMeta) {
+        return !(<TableSourceMeta>this.sourceMeta).is_view;
+      }
+      if (this.definition && this.definition.type) {
+        return this.definition.type.toLowerCase() === 'table';
+      }
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Returns true if the entry is a view. It will be accurate once the source meta has been loaded.
+   */
+  isView(): boolean {
+    if (this.path.length === 2) {
+      if (this.sourceMeta && (<TableSourceMeta>this.sourceMeta).is_view) {
+        return true;
+      }
+      if (
+        this.definition &&
+        this.definition.type &&
+        (this.definition.type.toLowerCase() === 'view' ||
+          this.definition.type.toLowerCase() === 'materialized_view')
+      ) {
+        return true;
+      }
+
+      if (
+        this.analysis &&
+        this.analysis.details &&
+        this.analysis.details.properties &&
+        this.analysis.details.properties.table_type === 'VIRTUAL_VIEW'
+      ) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Returns true if the entry is a ML Model. It will be accurate once the source meta has been loaded.
+   */
+  isModel(): boolean {
+    return (
+      this.path.length === 2 &&
+      !!this.definition &&
+      !!this.definition.type &&
+      this.definition.type.toLowerCase() === 'model'
+    );
+  }
+
+  /**
+   * Returns true if the entry is a column.
+   */
+  isColumn(): boolean {
+    return this.path.length === 3;
+  }
+
+  /**
+   * Returns true if the entry is a column. It will be accurate once the source meta has been loaded or if loaded from
+   * a parent entry via getChildren().
+   */
+  isComplex(): boolean {
+    const sourceMeta = <FieldSourceMeta>this.sourceMeta;
+    return !!(
+      this.path.length > 2 &&
+      ((sourceMeta && /^(?:struct|array|map)/i.test(sourceMeta.type)) ||
+        (this.definition &&
+          this.definition.type &&
+          /^(?:struct|array|map)/i.test(this.definition.type)))
+    );
+  }
+
+  /**
+   * Returns true if the entry is a field, i.e. column or child of a complex type.
+   */
+  isField(): boolean {
+    return this.path.length > 2;
+  }
+
+  /**
+   * Returns true if the entry is an array. It will be accurate once the source meta has been loaded or if loaded from
+   * a parent entry via getChildren().
+   */
+  isArray(): boolean {
+    return (
+      (!!this.sourceMeta && /^array/i.test((<FieldSourceMeta>this.sourceMeta).type)) ||
+      (!!this.definition && !!this.definition.type && /^array/i.test(this.definition.type))
+    );
+  }
+
+  /**
+   * Returns true if the entry is a map. It will be accurate once the source meta has been loaded or if loaded from
+   * a parent entry via getChildren().
+   */
+  isMap(): boolean {
+    return (
+      (!!this.sourceMeta && /^map/i.test((<FieldSourceMeta>this.sourceMeta).type)) ||
+      (!!this.definition && !!this.definition.type && /^map/i.test(this.definition.type))
+    );
+  }
+
+  /**
+   * Returns true if the entry is a map value. It will be accurate once the source meta has been loaded or if loaded
+   * from a parent entry via getChildren().
+   */
+  isMapValue(): boolean {
+    return !!this.definition && !!this.definition.isMapValue;
+  }
+
+  /**
+   * Returns the type of the entry. It will be accurate once the source meta has been loaded or if loaded from
+   * a parent entry via getChildren().
+   *
+   * The returned string is always lower case and for complex entries the type definition is stripped to
+   * either 'array', 'map' or 'struct'.
+   */
+  getType(): string {
+    let type = this.getRawType();
+    if (type.indexOf('<') !== -1) {
+      type = type.substring(0, type.indexOf('<'));
+    }
+    return type.toLowerCase();
+  }
+
+  /**
+   * Returns the raw type of the entry. It will be accurate once the source meta has been loaded or if loaded from
+   * a parent entry via getChildren().
+   *
+   * For complex entries the type definition is the full version.
+   */
+  getRawType(): string {
+    return (
+      (this.sourceMeta && (<FieldSourceMeta>this.sourceMeta).type) ||
+      (this.definition && this.definition.type) ||
+      ''
+    );
+  }
+
+  /**
+   * Gets the source metadata for the entry. It will fetch it if not cached or if the refresh option is set.
+   */
+  getSourceMeta(options?: CatalogGetOptions): CancellablePromise<SourceMeta> {
+    if (options && options.cachedOnly) {
+      return (
+        (this.sourceMetaPromise && applyCancellable(this.sourceMetaPromise, options)) ||
+        CancellablePromise.reject(false)
+      );
+    }
+    if (options && options.refreshCache) {
+      return applyCancellable(reloadSourceMeta(this, options));
+    }
+    return applyCancellable(this.sourceMetaPromise || reloadSourceMeta(this, options), options);
+  }
+
+  /**
+   * Gets the analysis for the entry. It will fetch it if not cached or if the refresh option is set.
+   */
+  getAnalysis(
+    options?: CatalogGetOptions & {
+      refreshAnalysis?: boolean;
+    }
+  ): CancellablePromise<Analysis> {
+    if (options && options.cachedOnly) {
+      return (
+        (this.analysisPromise && applyCancellable(this.analysisPromise, options)) ||
+        CancellablePromise.reject(false)
+      );
+    }
+    if (options && (options.refreshCache || options.refreshAnalysis)) {
+      return applyCancellable(reloadAnalysis(this, options), options);
+    }
+    return applyCancellable(this.analysisPromise || reloadAnalysis(this, options), options);
+  }
+
+  /**
+   * Gets the partitions for the entry. It will fetch it if not cached or if the refresh option is set.
+   */
+  getPartitions(options?: CatalogGetOptions): CancellablePromise<Partitions> {
+    if (!this.isTableOrView()) {
+      return CancellablePromise.reject();
+    }
+    if (options && options.cachedOnly) {
+      return (
+        (this.partitionsPromise && applyCancellable(this.partitionsPromise, options)) ||
+        CancellablePromise.reject()
+      );
+    }
+    if (options && options.refreshCache) {
+      return applyCancellable(reloadPartitions(this, options), options);
+    }
+    return applyCancellable(this.partitionsPromise || reloadPartitions(this, options), options);
+  }
+
+  /**
+   * Gets the Navigator metadata for the entry. It will fetch it if not cached or if the refresh option is set.
+   */
+  getNavigatorMeta(options?: CatalogGetOptions): CancellablePromise<NavigatorMeta> {
+    options = setSilencedErrors(options);
+
+    if (
+      !this.canHaveNavigatorMetadata() ||
+      (!this.navigatorMetaPromise && options && options.cachedOnly)
+    ) {
+      return CancellablePromise.reject();
+    }
+
+    if (
+      this.navigatorMetaPromise &&
+      DataCatalog.cacheEnabled() &&
+      (!options || !options.refreshCache)
+    ) {
+      return applyCancellable(this.navigatorMetaPromise, options);
+    }
+
+    return applyCancellable(reloadNavigatorMeta(this, options), options);
+  }
+
+  /**
+   * Gets the Nav Opt metadata for the entry. It will fetch it if not cached or if the refresh option is set.
+   */
+  getOptimizerMeta(options?: CatalogGetOptions): CancellablePromise<OptimizerMeta> {
+    options = setSilencedErrors(options);
+
+    if (!this.dataCatalog.canHaveOptimizerMeta() || !this.isTableOrView()) {
+      return CancellablePromise.reject();
+    }
+    if (options && options.cachedOnly) {
+      return (
+        (this.optimizerMetaPromise && applyCancellable(this.optimizerMetaPromise, options)) ||
+        CancellablePromise.reject()
+      );
+    }
+    if (options && options.refreshCache) {
+      return applyCancellable(reloadOptimizerMeta(this, options), options);
+    }
+    return applyCancellable(
+      this.optimizerMetaPromise || reloadOptimizerMeta(this, options),
+      options
+    );
+  }
+
+  /**
+   * Gets the sample for the entry, if unknown it will first check if any parent table already has the sample. It
+   * will fetch it if not cached or if the refresh option is set.
+   */
+  getSample(
+    options?: CatalogGetOptions & {
+      operation?: string;
+    }
+  ): CancellablePromise<Sample> {
+    // This prevents caching of any non-standard sample queries, i.e. DISTINCT etc.
+    if (options && options.operation && options.operation !== 'default') {
+      const operation = options.operation;
+      const samplePromise = new CancellablePromise<Sample>((resolve, reject, onCancel) => {
+        const fetchPromise = apiHelper.fetchSample({
+          sourceType: this.getConnector().id,
+          compute: this.compute,
+          path: this.path,
+          silenceErrors: !!(options && options.silenceErrors),
+          operation: operation
+        });
+        onCancel(() => {
+          fetchPromise.cancel();
+        });
+
+        fetchPromise.done(resolve).fail(reject);
+      });
+      return applyCancellable(samplePromise, options);
+    }
+
+    // Check if parent has a sample that we can reuse
+    if (!this.samplePromise && this.isColumn()) {
+      const samplePromise = new CancellablePromise<Sample>(async (resolve, reject, onCancel) => {
+        const cancellablePromises: Cancellable[] = [];
+
+        onCancel(() => {
+          cancellablePromises.forEach(promise => promise.cancel());
+        });
+
+        try {
+          const tableEntry = await this.dataCatalog.getEntry({
+            namespace: this.namespace,
+            compute: this.compute,
+            path: this.path.slice(0, 2),
+            definition: { type: 'table' }
+          });
+
+          if (tableEntry && tableEntry.samplePromise) {
+            cancellablePromises.push(applyCancellable(tableEntry.samplePromise, options));
+
+            const parentSample = await tableEntry.samplePromise;
+
+            const colSample = {
+              hueTimestamp: parentSample.hueTimestamp,
+              has_more: parentSample.has_more,
+              type: parentSample.type,
+              data: <FieldSample[][]>[],
+              meta: <SampleMeta[]>[]
+            };
+            if (parentSample.meta) {
+              for (let i = 0; i < parentSample.meta.length; i++) {
+                if (parentSample.meta[i].name.toLowerCase() === self.name.toLowerCase()) {
+                  colSample.meta[0] = parentSample.meta[i];
+                  parentSample.data.forEach(parentRow => {
+                    colSample.data.push([parentRow[i]]);
+                  });
+                  break;
+                }
+              }
+            }
+            if (colSample.meta.length) {
+              this.sample = colSample;
+              resolve(this.sample);
+              return;
+            }
+          }
+        } catch (err) {}
+
+        if (options && options.cachedOnly) {
+          reject();
+        } else {
+          const reloadPromise = applyCancellable(reloadSample(this, options), options);
+          cancellablePromises.push(reloadPromise);
+          try {
+            resolve(await reloadPromise);
+          } catch (err) {
+            reject();
+          }
+        }
+      });
+
+      return applyCancellable(samplePromise, options);
+    }
+
+    if (options && options.cachedOnly) {
+      return (
+        (this.samplePromise && applyCancellable(this.samplePromise, options)) ||
+        CancellablePromise.reject()
+      );
+    }
+    if (options && options.refreshCache) {
+      return applyCancellable(reloadSample(this, options), options);
+    }
+    return applyCancellable(this.samplePromise || reloadSample(this, options), options);
+  }
+
+  /**
+   * Gets the top aggregate UDFs for the entry if it's a table or view. It will fetch it if not cached or if the refresh option is set.
+   */
+  getTopAggs(options?: CatalogGetOptions): CancellablePromise<TopAggs> {
+    const promise = new CancellablePromise<TopAggs>(async (resolve, reject, onCancel) => {
+      const multiTableEntry = await getMultiTableEntry(this);
+      const topAggsPromise = multiTableEntry.getTopAggs(options);
+      onCancel(() => {
+        topAggsPromise.cancel();
+      });
+      topAggsPromise.then(resolve).catch(reject);
+    });
+    return applyCancellable(promise);
+  }
+
+  /**
+   * Gets the top filters for the entry if it's a table or view. It will fetch it if not cached or if the refresh option is set.
+   *
+   * @return {CancellableJqPromise}
+   */
+  getTopFilters(options?: CatalogGetOptions): CancellablePromise<TopFilters> {
+    const promise = new CancellablePromise<TopFilters>(async (resolve, reject, onCancel) => {
+      const multiTableEntry = await getMultiTableEntry(this);
+      const topFiltersPromise = multiTableEntry.getTopFilters(options);
+      onCancel(() => {
+        topFiltersPromise.cancel();
+      });
+      topFiltersPromise.then(resolve).catch(reject);
+    });
+    return applyCancellable(promise);
+  }
+
+  /**
+   * Gets the top joins for the entry if it's a table or view. It will fetch it if not cached or if the refresh option is set.
+   */
+  getTopJoins(options?: CatalogGetOptions): CancellablePromise<TopJoins> {
+    const promise = new CancellablePromise<TopJoins>(async (resolve, reject, onCancel) => {
+      const multiTableEntry = await getMultiTableEntry(this);
+      const topJoinsPromise = multiTableEntry.getTopJoins(options);
+      onCancel(() => {
+        topJoinsPromise.cancel();
+      });
+      topJoinsPromise.then(resolve).catch(reject);
+    });
+    return applyCancellable(promise);
+  }
+}

+ 137 - 0
desktop/core/src/desktop/js/catalog/GeneralDataCatalog.ts

@@ -0,0 +1,137 @@
+// 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.
+
+import localforage from 'localforage';
+
+import apiHelper from 'api/apiHelper';
+import { hueWindow } from 'types/types';
+import { DataCatalog } from './dataCatalog';
+
+export interface Tags {
+  [tag: string]: number;
+}
+
+interface TagsStoreEntry {
+  allTags: Tags;
+  hueTimestamp: number;
+  version: number;
+}
+
+const STORAGE_POSTFIX = (<hueWindow>window).LOGGED_USERNAME;
+const DATA_CATALOG_VERSION = 5;
+const NAV_TAGS_STORE_ID = 'hue.dataCatalog.allNavTags';
+
+export default class GeneralDataCatalog {
+  store: LocalForage;
+  allNavigatorTagsPromise?: Promise<Tags>;
+
+  constructor() {
+    this.store = localforage.createInstance({
+      name: 'HueDataCatalog_' + STORAGE_POSTFIX
+    });
+  }
+
+  async getAllNavigatorTags(options?: {
+    silenceErrors?: boolean;
+    refreshCache?: boolean;
+  }): Promise<Tags> {
+    if (
+      this.allNavigatorTagsPromise &&
+      DataCatalog.cacheEnabled() &&
+      (!options || !options.refreshCache)
+    ) {
+      return this.allNavigatorTagsPromise;
+    }
+
+    if (!(<hueWindow>window).HAS_CATALOG) {
+      return {};
+    }
+
+    const ttl = (<hueWindow>window).CACHEABLE_TTL || {};
+
+    this.allNavigatorTagsPromise = new Promise((resolve, reject) => {
+      const reloadAllTags = () => {
+        apiHelper
+          .fetchAllNavigatorTags({
+            silenceErrors: options && options.silenceErrors
+          })
+          .then(allTags => {
+            resolve(allTags);
+            if (ttl.default && ttl.default > 0) {
+              this.store.setItem<TagsStoreEntry>(NAV_TAGS_STORE_ID, {
+                allTags,
+                hueTimestamp: Date.now(),
+                version: DATA_CATALOG_VERSION
+              });
+            }
+          })
+          .fail(reject);
+      };
+
+      if (
+        ttl.default &&
+        ttl.default > 0 &&
+        DataCatalog.cacheEnabled() &&
+        (!options || !options.refreshCache)
+      ) {
+        this.store
+          .getItem<TagsStoreEntry>(NAV_TAGS_STORE_ID)
+          .then(storeEntry => {
+            if (
+              storeEntry &&
+              storeEntry.version === DATA_CATALOG_VERSION &&
+              ttl.default &&
+              (!storeEntry.hueTimestamp || Date.now() - storeEntry.hueTimestamp < ttl.default)
+            ) {
+              resolve(storeEntry.allTags);
+            } else {
+              reloadAllTags();
+            }
+          })
+          .catch(reloadAllTags);
+      } else {
+        reloadAllTags();
+      }
+    });
+
+    return this.allNavigatorTagsPromise;
+  }
+
+  async updateAllNavigatorTags(tagsToAdd: string[], tagsToRemove: string[]): Promise<void> {
+    if (this.allNavigatorTagsPromise) {
+      const allTags = await this.allNavigatorTagsPromise;
+      tagsToAdd.forEach(newTag => {
+        if (!allTags[newTag]) {
+          allTags[newTag] = 0;
+        }
+        allTags[newTag]++;
+      });
+      tagsToRemove.forEach(removedTag => {
+        if (!allTags[removedTag]) {
+          allTags[removedTag]--;
+          if (allTags[removedTag] === 0) {
+            delete allTags[removedTag];
+          }
+        }
+      });
+      await this.store.setItem<TagsStoreEntry>(NAV_TAGS_STORE_ID, {
+        allTags: allTags,
+        hueTimestamp: Date.now(),
+        version: DATA_CATALOG_VERSION
+      });
+    }
+  }
+}

+ 280 - 0
desktop/core/src/desktop/js/catalog/MultiTableEntry.ts

@@ -0,0 +1,280 @@
+// 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.
+
+import { CatalogGetOptions, DataCatalog, TimestampedData } from './dataCatalog';
+import CancellableJqPromise from 'api/cancellableJqPromise';
+import { CancellablePromise } from 'api/cancellablePromise';
+import { applyCancellable, fetchAndSave, FetchOptions } from 'catalog/catalogUtils';
+import { getOptimizer } from 'catalog/optimizer/optimizer';
+import { UdfDetails } from 'sql/reference/types';
+import { Connector } from 'types/config';
+import { hueWindow } from 'types/types';
+
+export interface TopJoinValue {
+  totalQueryCount: number;
+  joinType: string;
+  tables: string[];
+  joinCols: { columns: string[] }[];
+  relativePopularity?: number;
+}
+
+export interface TopJoins extends TimestampedData {
+  values: TopJoinValue[];
+}
+
+export interface TopAggValue {
+  aggregateClause: string;
+  aggregateFunction: string;
+  aggregateInfo: {
+    columnName: string;
+    databaseName: string;
+    tableName: string;
+  }[];
+  function?: UdfDetails;
+  relativePopularity?: number;
+  totalQueryCount: number;
+}
+
+export interface TopAggs extends TimestampedData {
+  values: TopAggValue[];
+}
+
+export interface TopFilterValue {
+  count: number;
+  relativePopularity?: number;
+  group?: {
+    columnName: string;
+    op: string;
+    literal: string;
+  }[];
+}
+
+export interface TopFilters extends TimestampedData {
+  values: {
+    tableName: string;
+    count: number;
+    popularValues?: TopFilterValue[];
+  }[];
+}
+
+export interface TopColumns extends TimestampedData {
+  values: unknown[];
+}
+
+/**
+ * Helper function to reload a Optimizer multi table attribute, like topAggs or topFilters
+ */
+const genericOptimizerReload = <T>(
+  multiTableEntry: MultiTableEntry,
+  options: { silenceErrors?: boolean } | undefined,
+  promiseSetter: (promise?: CancellablePromise<T>) => void,
+  dataAttributeSetter: (val: T) => void,
+  apiHelperFunction: (option: FetchOptions) => CancellableJqPromise<T>
+): CancellablePromise<T> => {
+  const promise = new CancellablePromise<T>((resolve, reject, onCancel) => {
+    if (!multiTableEntry.dataCatalog.canHaveOptimizerMeta()) {
+      reject();
+      return;
+    }
+    const fetchPromise = fetchAndSave(
+      apiHelperFunction,
+      dataAttributeSetter,
+      multiTableEntry,
+      options
+    );
+    onCancel(() => {
+      if (fetchPromise.cancel) {
+        fetchPromise.cancel();
+      }
+    });
+
+    fetchPromise.done(resolve).fail(() => {
+      if (fetchPromise.cancelled) {
+        promiseSetter(undefined);
+      }
+      reject();
+    });
+  });
+
+  promiseSetter(promise);
+  return promise;
+};
+
+/**
+ * Helper function to get a Optimizer multi table attribute, like topAggs or topFilters
+ */
+const genericOptimizerGet = <T>(
+  multiTableEntry: MultiTableEntry,
+  options: CatalogGetOptions | undefined,
+  promiseSetter: (promise?: CancellablePromise<T>) => void,
+  promiseGetter: () => CancellablePromise<T> | undefined,
+  dataAttributeSetter: (val: T) => void,
+  apiHelperFunction: (option: FetchOptions) => CancellableJqPromise<T>
+): CancellablePromise<T> => {
+  let promise = promiseGetter();
+  if (DataCatalog.cacheEnabled() && options && options.cachedOnly) {
+    return (promise && applyCancellable(promise)) || CancellablePromise.reject();
+  }
+
+  if (!promise || !DataCatalog.cacheEnabled() || (options && options.refreshCache)) {
+    promise = genericOptimizerReload<T>(
+      multiTableEntry,
+      options,
+      promiseSetter,
+      dataAttributeSetter,
+      apiHelperFunction
+    );
+  }
+
+  return applyCancellable(promise, options);
+};
+
+class MultiTableEntry {
+  dataCatalog: DataCatalog;
+  identifier: string;
+  paths: string[][];
+  saveTimeout = -1;
+
+  topAggs?: TopAggs;
+  topAggsPromise?: CancellablePromise<TopAggs>;
+  topColumns?: TopColumns;
+  topColumnsPromise?: CancellablePromise<TopColumns>;
+  topFilters?: TopFilters;
+  topFiltersPromise?: CancellablePromise<TopFilters>;
+  topJoins?: TopJoins;
+  topJoinsPromise?: CancellablePromise<TopJoins>;
+
+  constructor(options: { identifier: string; dataCatalog: DataCatalog; paths: string[][] }) {
+    this.identifier = options.identifier;
+    this.dataCatalog = options.dataCatalog;
+    this.paths = options.paths;
+  }
+
+  /**
+   * Save the multi table entry to cache
+   *
+   * @return {Promise}
+   */
+  save(): Promise<void> {
+    window.clearTimeout(this.saveTimeout);
+    return this.dataCatalog.persistMultiTableEntry(this);
+  }
+
+  /**
+   * Save the multi table entry at a later point of time
+   */
+  saveLater(): void {
+    const ttl = (<hueWindow>window).CACHEABLE_TTL;
+    if (ttl && ttl.default && ttl.default > 0) {
+      window.clearTimeout(this.saveTimeout);
+      this.saveTimeout = window.setTimeout(() => {
+        this.save();
+      }, 1000);
+    }
+  }
+
+  /**
+   * Returns the dialect of this entry.
+   */
+  getDialect(): string {
+    return this.getConnector().dialect || this.getConnector().id; // .id for editor v1
+  }
+
+  /**
+   * Returns the connector for this entry
+   */
+  getConnector(): Connector {
+    return this.dataCatalog.connector;
+  }
+
+  /**
+   * Gets the top aggregate UDFs for the entry. It will fetch it if not cached or if the refresh option is set.
+   */
+  getTopAggs(options?: CatalogGetOptions): CancellablePromise<TopAggs> {
+    const optimizer = getOptimizer(this.dataCatalog.connector);
+    return genericOptimizerGet<TopAggs>(
+      this,
+      options,
+      promise => {
+        this.topAggsPromise = promise;
+      },
+      () => this.topAggsPromise,
+      val => {
+        this.topAggs = val;
+      },
+      optimizer.fetchTopAggs.bind(optimizer)
+    );
+  }
+
+  /**
+   * Gets the top columns for the entry. It will fetch it if not cached or if the refresh option is set.
+   */
+  getTopColumns(options?: CatalogGetOptions): CancellablePromise<TopColumns> {
+    const optimizer = getOptimizer(this.dataCatalog.connector);
+    return genericOptimizerGet<TopColumns>(
+      this,
+      options,
+      promise => {
+        this.topColumnsPromise = promise;
+      },
+      () => this.topColumnsPromise,
+      val => {
+        this.topColumns = val;
+      },
+      optimizer.fetchTopColumns.bind(optimizer)
+    );
+  }
+
+  /**
+   * Gets the top filters for the entry. It will fetch it if not cached or if the refresh option is set.
+   */
+  getTopFilters(options?: CatalogGetOptions): CancellablePromise<TopFilters> {
+    const optimizer = getOptimizer(this.dataCatalog.connector);
+    return genericOptimizerGet<TopFilters>(
+      this,
+      options,
+      promise => {
+        this.topFiltersPromise = promise;
+      },
+      () => this.topFiltersPromise,
+      val => {
+        this.topFilters = val;
+      },
+      optimizer.fetchTopFilters.bind(optimizer)
+    );
+  }
+
+  /**
+   * Gets the top joins for the entry. It will fetch it if not cached or if the refresh option is set.
+   */
+  getTopJoins(options?: CatalogGetOptions): CancellablePromise<TopJoins> {
+    const optimizer = getOptimizer(this.dataCatalog.connector);
+    return genericOptimizerGet<TopJoins>(
+      this,
+      options,
+      promise => {
+        this.topJoinsPromise = promise;
+      },
+      () => this.topJoinsPromise,
+      val => {
+        this.topJoins = val;
+      },
+      optimizer.fetchTopJoins.bind(optimizer)
+    );
+  }
+}
+
+export default MultiTableEntry;

+ 38 - 30
desktop/core/src/desktop/js/catalog/catalogUtils.js → desktop/core/src/desktop/js/catalog/catalogUtils.ts

@@ -14,36 +14,53 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import CancellableJqPromise from 'api/cancellableJqPromise';
+import { CancellablePromise } from 'api/cancellablePromise';
+import DataCatalogEntry from 'catalog/DataCatalogEntry';
+import MultiTableEntry from 'catalog/MultiTableEntry';
+import { Compute, Connector } from 'types/config';
+
+export interface FetchOptions {
+  sourceType: string;
+  compute?: Compute;
+  path?: string[];
+  paths?: string[][];
+  silenceErrors?: boolean;
+  connector: Connector;
+  isView?: boolean;
+}
+
 /**
  * Wrapper function around ApiHelper calls, it will also save the entry on success.
- *
- * @param {Function} apiHelperFunction - The name of the ApiHelper function to call
- * @param {Function} setFunction - The attribute to set
- * @param {DataCatalogEntry|MultiTableEntry} entry - The catalog entry
- * @param {Object} [apiOptions]
- * @param {boolean} [apiOptions.silenceErrors]
  */
-const fetchAndSave = (apiHelperFunction, setFunction, entry, apiOptions) => {
-  return apiHelperFunction({
+export const fetchAndSave = <T>(
+  apiHelperFunction: (option: FetchOptions) => CancellableJqPromise<T>,
+  setFunction: (val: T) => void,
+  entry: DataCatalogEntry | MultiTableEntry,
+  apiOptions?: { silenceErrors?: boolean; refreshAnalysis?: boolean }
+): CancellableJqPromise<T> => {
+  const promise = apiHelperFunction({
     sourceType: entry.getConnector().id,
-    compute: entry.compute,
-    path: entry.path, // Set for DataCatalogEntry
-    paths: entry.paths, // Set for MultiTableEntry
+    compute: (<DataCatalogEntry>entry).compute,
+    path: (<DataCatalogEntry>entry).path, // Set for DataCatalogEntry
+    paths: (<MultiTableEntry>entry).paths, // Set for MultiTableEntry
     silenceErrors: apiOptions && apiOptions.silenceErrors,
     connector: entry.dataCatalog.connector,
-    isView: entry.isView && entry.isView() // MultiTable entries don't have this property
-  }).done(data => {
+    isView: (<DataCatalogEntry>entry).isView && (<DataCatalogEntry>entry).isView() // MultiTable entries don't have this property
+  });
+  promise.then(data => {
     setFunction(data);
     entry.saveLater();
   });
+  return promise;
 };
+
 /**
  * Helper function that adds sets the silence errors option to true if not specified
- *
- * @param {Object} [options]
- * @return {Object}
  */
-const setSilencedErrors = options => {
+export const setSilencedErrors = (options?: {
+  silenceErrors?: boolean;
+}): { silenceErrors?: boolean } => {
   if (!options) {
     options = {};
   }
@@ -55,22 +72,13 @@ const setSilencedErrors = options => {
 
 /**
  * Helper function to apply the cancellable option to an existing or new promise
- *
- * @param {CancellableJqPromise} [promise]
- * @param {Object} [options]
- * @param {boolean} [options.cancellable] - Default false
- *
- * @return {CancellableJqPromise}
  */
-const applyCancellable = (promise, options) => {
+export const applyCancellable = <T>(
+  promise: CancellablePromise<T>,
+  options?: { cancellable?: boolean }
+): CancellablePromise<T> => {
   if (promise && promise.preventCancel && (!options || !options.cancellable)) {
     promise.preventCancel();
   }
   return promise;
 };
-
-export default {
-  applyCancellable: applyCancellable,
-  fetchAndSave: fetchAndSave,
-  setSilencedErrors: setSilencedErrors
-};

+ 0 - 972
desktop/core/src/desktop/js/catalog/dataCatalog.js

@@ -1,972 +0,0 @@
-// 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.
-
-import $ from 'jquery';
-import localforage from 'localforage';
-
-import CancellableJqPromise from 'api/cancellableJqPromise';
-import catalogUtils from 'catalog/catalogUtils';
-import DataCatalogEntry from 'catalog/dataCatalogEntry';
-import GeneralDataCatalog from 'catalog/generalDataCatalog';
-import MultiTableEntry from 'catalog/multiTableEntry';
-import { getOptimizer, LOCAL_STRATEGY } from './optimizer/optimizer';
-
-const STORAGE_POSTFIX = window.LOGGED_USERNAME;
-const DATA_CATALOG_VERSION = 5;
-
-let cacheEnabled = true;
-
-/**
- * Creates a cache identifier given a namespace and path(s)
- *
- * @param {Object|DataCatalogEntry} options
- * @param {Object} options.namespace
- * @param {string} options.namespace.id
- * @param {string[]} [options.path]
- * @param {string[][]} [options.paths]
- * @return {string}
- */
-const generateEntryCacheId = function (options) {
-  let id = options.namespace.id;
-  if (options.path) {
-    if (typeof options.path === 'string') {
-      id += '_' + options.path;
-    } else if (options.path.length) {
-      id += '_' + options.path.join('.');
-    }
-  } else if (options.paths && options.paths.length) {
-    const pathSet = {};
-    options.paths.forEach(path => {
-      pathSet[path.join('.')] = true;
-    });
-    const uniquePaths = Object.keys(pathSet);
-    uniquePaths.sort();
-    id += '_' + uniquePaths.join(',');
-  }
-  return id;
-};
-
-const isFresh = (storeEntryValue, ttl) =>
-  !storeEntryValue.hueTimestamp ||
-  Date.now() - storeEntryValue.hueTimestamp < (ttl || CACHEABLE_TTL.default);
-
-/**
- * Helper function to fill a catalog entry with cached metadata.
- *
- * @param {DataCatalogEntry} dataCatalogEntry - The entry to fill
- * @param {Object} storeEntry - The cached version
- */
-const mergeEntry = function (dataCatalogEntry, storeEntry) {
-  if (storeEntry.version !== DATA_CATALOG_VERSION) {
-    return;
-  }
-
-  if (storeEntry.definition && isFresh(storeEntry.definition)) {
-    dataCatalogEntry.definition = storeEntry.definition;
-  }
-  if (storeEntry.sourceMeta && isFresh(storeEntry.sourceMeta)) {
-    dataCatalogEntry.sourceMeta = storeEntry.sourceMeta;
-    dataCatalogEntry.sourceMetaPromise = $.Deferred()
-      .resolve(dataCatalogEntry.sourceMeta)
-      .promise();
-  }
-  if (storeEntry.analysis && isFresh(storeEntry.analysis)) {
-    dataCatalogEntry.analysis = storeEntry.analysis;
-    dataCatalogEntry.analysisPromise = $.Deferred().resolve(dataCatalogEntry.analysis).promise();
-  }
-  if (storeEntry.partitions && isFresh(storeEntry.partitions)) {
-    dataCatalogEntry.partitions = storeEntry.partitions;
-    dataCatalogEntry.partitionsPromise = $.Deferred()
-      .resolve(dataCatalogEntry.partitions)
-      .promise();
-  }
-  if (storeEntry.sample && isFresh(storeEntry.sample)) {
-    dataCatalogEntry.sample = storeEntry.sample;
-    dataCatalogEntry.samplePromise = $.Deferred().resolve(dataCatalogEntry.sample).promise();
-  }
-  if (storeEntry.navigatorMeta && isFresh(storeEntry.navigatorMeta)) {
-    dataCatalogEntry.navigatorMeta = storeEntry.navigatorMeta;
-    dataCatalogEntry.navigatorMetaPromise = $.Deferred()
-      .resolve(dataCatalogEntry.navigatorMeta)
-      .promise();
-  }
-  if (dataCatalogEntry.getConnector().optimizer !== LOCAL_STRATEGY) {
-    if (storeEntry.optimizerMeta && isFresh(storeEntry.optimizerMeta, CACHEABLE_TTL.optimizer)) {
-      dataCatalogEntry.optimizerMeta = storeEntry.optimizerMeta;
-      dataCatalogEntry.optimizerMetaPromise = $.Deferred()
-        .resolve(dataCatalogEntry.optimizerMeta)
-        .promise();
-    }
-    if (
-      storeEntry.optimizerPopularity &&
-      isFresh(storeEntry.optimizerPopularity, CACHEABLE_TTL.optimizer)
-    ) {
-      dataCatalogEntry.optimizerPopularity = storeEntry.optimizerPopularity;
-      dataCatalogEntry.optimizerMetaPromise = $.Deferred()
-        .resolve(dataCatalogEntry.optimizerPopularity)
-        .promise();
-    }
-  }
-};
-
-/**
- * Helper function to fill a multi table catalog entry with cached metadata.
- *
- * @param {MultiTableEntry} multiTableCatalogEntry - The entry to fill
- * @param {Object} storeEntry - The cached version
- */
-const mergeMultiTableEntry = function (multiTableCatalogEntry, storeEntry) {
-  if (
-    multiTableCatalogEntry.getConnector().optimizer === LOCAL_STRATEGY ||
-    storeEntry.version !== DATA_CATALOG_VERSION
-  ) {
-    return;
-  }
-  if (storeEntry.topAggs && isFresh(storeEntry.topAggs, CACHEABLE_TTL.optimizer)) {
-    multiTableCatalogEntry.topAggs = storeEntry.topAggs;
-    multiTableCatalogEntry.topAggsPromise = $.Deferred()
-      .resolve(multiTableCatalogEntry.topAggs)
-      .promise();
-  }
-  if (storeEntry.topColumns && isFresh(storeEntry.topColumns, CACHEABLE_TTL.optimizer)) {
-    multiTableCatalogEntry.topColumns = storeEntry.topColumns;
-    multiTableCatalogEntry.topColumnsPromise = $.Deferred()
-      .resolve(multiTableCatalogEntry.topColumns)
-      .promise();
-  }
-  if (storeEntry.topFilters && isFresh(storeEntry.topFilters, CACHEABLE_TTL.optimizer)) {
-    multiTableCatalogEntry.topFilters = storeEntry.topFilters;
-    multiTableCatalogEntry.topFiltersPromise = $.Deferred()
-      .resolve(multiTableCatalogEntry.topFilters)
-      .promise();
-  }
-  if (storeEntry.topJoins && isFresh(storeEntry.topJoins, CACHEABLE_TTL.optimizer)) {
-    multiTableCatalogEntry.topJoins = storeEntry.topJoins;
-    multiTableCatalogEntry.topJoinsPromise = $.Deferred()
-      .resolve(multiTableCatalogEntry.topJoins)
-      .promise();
-  }
-};
-
-export class DataCatalog {
-  /**
-   * @param {Connector} connector
-   *
-   * @constructor
-   */
-  constructor(connector) {
-    const self = this;
-    if (!connector || !connector.id) {
-      throw new Error('DataCatalog created without connector or id');
-    }
-    self.connector = connector;
-
-    self.entries = {};
-    self.temporaryEntries = {};
-    self.multiTableEntries = {};
-    self.store = localforage.createInstance({
-      name: 'HueDataCatalog_' + self.connector.id + '_' + STORAGE_POSTFIX
-    });
-    self.multiTableStore = localforage.createInstance({
-      name: 'HueDataCatalog_' + self.connector.id + '_multiTable_' + STORAGE_POSTFIX
-    });
-  }
-
-  /**
-   * Disables the caching for subsequent operations, mainly used for test purposes
-   */
-  static disableCache() {
-    cacheEnabled = false;
-  }
-
-  /**
-   * Enables the cache for subsequent operations, mainly used for test purposes
-   */
-  static enableCache() {
-    cacheEnabled = true;
-  }
-
-  static cacheEnabled() {
-    return cacheEnabled;
-  }
-
-  /**
-   * Returns true if the catalog can have Optimizer metadata
-   *
-   * @return {boolean}
-   */
-  canHaveOptimizerMeta() {
-    return (
-      HAS_OPTIMIZER &&
-      this.connector &&
-      this.connector.optimizer &&
-      this.connector.optimizer !== 'off'
-    );
-  }
-
-  /**
-   * Clears the data catalog and cache for the given path and any children thereof.
-   *
-   * @param {ContextNamespace} [namespace] - The context namespace
-   * @param {ContextCompute} [compute] - The context compute
-   * @param {string[]} rootPath - The path to clear
-   */
-  clearStorageCascade(namespace, compute, rootPath) {
-    const self = this;
-    const deferred = $.Deferred();
-    if (!namespace || !compute) {
-      if (rootPath.length === 0) {
-        self.entries = {};
-        self.store.clear().then(deferred.resolve).catch(deferred.reject);
-        return deferred.promise();
-      }
-      return deferred.reject().promise();
-    }
-
-    const keyPrefix = generateEntryCacheId({ namespace: namespace, path: rootPath });
-    Object.keys(self.entries).forEach(key => {
-      if (key.indexOf(keyPrefix) === 0) {
-        delete self.entries[key];
-      }
-    });
-
-    const deletePromises = [];
-    const keysDeferred = $.Deferred();
-    deletePromises.push(keysDeferred.promise());
-    self.store
-      .keys()
-      .then(keys => {
-        keys.forEach(key => {
-          if (key.indexOf(keyPrefix) === 0) {
-            const deleteDeferred = $.Deferred();
-            deletePromises.push(deleteDeferred.promise());
-            self.store.removeItem(key).then(deleteDeferred.resolve).catch(deleteDeferred.reject);
-          }
-        });
-        keysDeferred.resolve();
-      })
-      .catch(keysDeferred.reject);
-
-    return $.when.apply($, deletePromises);
-  }
-
-  /**
-   * Updates the cache for the given entry
-   *
-   * @param {DataCatalogEntry} dataCatalogEntry
-   * @return {Promise}
-   */
-  persistCatalogEntry(dataCatalogEntry) {
-    const self = this;
-    if (!cacheEnabled || CACHEABLE_TTL.default <= 0) {
-      return $.Deferred().resolve().promise();
-    }
-    const deferred = $.Deferred();
-
-    const identifier = generateEntryCacheId(dataCatalogEntry);
-
-    self.store
-      .setItem(identifier, {
-        version: DATA_CATALOG_VERSION,
-        definition: dataCatalogEntry.definition,
-        sourceMeta: dataCatalogEntry.sourceMeta,
-        analysis: dataCatalogEntry.analysis,
-        partitions: dataCatalogEntry.partitions,
-        sample: dataCatalogEntry.sample,
-        navigatorMeta: dataCatalogEntry.navigatorMeta,
-        optimizerMeta:
-          this.connector.optimizer !== LOCAL_STRATEGY ? dataCatalogEntry.optimizerMeta : undefined,
-        optimizerPopularity:
-          this.connector.optimizer !== LOCAL_STRATEGY
-            ? dataCatalogEntry.optimizerPopularity
-            : undefined
-      })
-      .then(deferred.resolve)
-      .catch(deferred.reject);
-
-    return deferred.promise();
-  }
-
-  /**
-   * Loads Navigator Optimizer popularity for multiple tables in one go.
-   *
-   * @param {Object} options
-   * @param {ContextNamespace} options.namespace - The context namespace
-   * @param {ContextCompute} options.compute - The context compute
-   * @param {string[][]} options.paths
-   * @param {boolean} [options.silenceErrors] - Default true
-   * @param {boolean} [options.cancellable] - Default false
-   *
-   * @return {CancellableJqPromise}
-   */
-  loadOptimizerPopularityForTables(options) {
-    const deferred = $.Deferred();
-    const cancellablePromises = [];
-    let popularEntries = [];
-    const pathsToLoad = [];
-
-    options = catalogUtils.setSilencedErrors(options);
-
-    const existingPromises = [];
-    options.paths.forEach(path => {
-      const existingDeferred = $.Deferred();
-      this.getEntry({ namespace: options.namespace, compute: options.compute, path: path })
-        .done(tableEntry => {
-          if (tableEntry.optimizerPopularityForChildrenPromise) {
-            tableEntry.optimizerPopularityForChildrenPromise
-              .done(existingPopularEntries => {
-                popularEntries = popularEntries.concat(existingPopularEntries);
-                existingDeferred.resolve();
-              })
-              .fail(existingDeferred.reject);
-          } else if (tableEntry.definition && tableEntry.definition.optimizerLoaded) {
-            cancellablePromises.push(
-              tableEntry
-                .getChildren(options)
-                .done(childEntries => {
-                  childEntries.forEach(childEntry => {
-                    if (childEntry.optimizerPopularity) {
-                      popularEntries.push(childEntry);
-                    }
-                  });
-                  existingDeferred.resolve();
-                })
-                .fail(existingDeferred.reject)
-            );
-          } else {
-            pathsToLoad.push(path);
-            existingDeferred.resolve();
-          }
-        })
-        .fail(existingDeferred.reject);
-      existingPromises.push(existingDeferred.promise());
-    });
-
-    $.when.apply($, existingPromises).always(() => {
-      const loadDeferred = $.Deferred();
-      if (pathsToLoad.length) {
-        cancellablePromises.push(
-          getOptimizer(this.connector)
-            .fetchPopularity({
-              silenceErrors: options.silenceErrors,
-              paths: pathsToLoad
-            })
-            .done(data => {
-              const perTable = {};
-
-              const splitOptimizerValuesPerTable = function (listName) {
-                if (data.values[listName]) {
-                  data.values[listName].forEach(column => {
-                    let tableMeta = perTable[column.dbName + '.' + column.tableName];
-                    if (!tableMeta) {
-                      tableMeta = { values: [] };
-                      perTable[column.dbName + '.' + column.tableName] = tableMeta;
-                    }
-                    if (!tableMeta.values[listName]) {
-                      tableMeta.values[listName] = [];
-                    }
-                    tableMeta.values[listName].push(column);
-                  });
-                }
-              };
-
-              if (data.values) {
-                splitOptimizerValuesPerTable('filterColumns');
-                splitOptimizerValuesPerTable('groupbyColumns');
-                splitOptimizerValuesPerTable('joinColumns');
-                splitOptimizerValuesPerTable('orderbyColumns');
-                splitOptimizerValuesPerTable('selectColumns');
-              }
-
-              const tablePromises = [];
-
-              Object.keys(perTable).forEach(path => {
-                const tableDeferred = $.Deferred();
-                this.getEntry({
-                  namespace: options.namespace,
-                  compute: options.compute,
-                  path: path
-                })
-                  .done(entry => {
-                    cancellablePromises.push(
-                      entry.trackedPromise(
-                        promise => {
-                          entry.optimizerPopularityForChildrenPromise = promise;
-                        },
-                        entry
-                          .applyOptimizerResponseToChildren(perTable[path], options)
-                          .done(entries => {
-                            popularEntries = popularEntries.concat(entries);
-                            tableDeferred.resolve();
-                          })
-                          .fail(tableDeferred.resolve)
-                      )
-                    );
-                  })
-                  .fail(tableDeferred.reject);
-                tablePromises.push(tableDeferred.promise());
-              });
-
-              $.when.apply($, tablePromises).always(() => {
-                loadDeferred.resolve();
-              });
-            })
-            .fail(loadDeferred.reject)
-        );
-      } else {
-        loadDeferred.resolve();
-      }
-      loadDeferred.always(() => {
-        $.when
-          .apply(
-            $,
-            cancellablePromises.map(cancellable => cancellable.deferred)
-          )
-          .done(() => {
-            deferred.resolve(popularEntries);
-          })
-          .fail(deferred.reject);
-      });
-    });
-
-    return catalogUtils.applyCancellable(
-      new CancellableJqPromise(deferred, cancellablePromises),
-      options
-    );
-  }
-
-  /**
-   * @param {Object} options
-   * @param {ContextNamespace} options.namespace - The context namespace
-   * @param {ContextCompute} options.compute - The context compute
-   * @param {string|string[]} options.path
-   * @return {DataCatalogEntry}
-   */
-  getKnownEntry(options) {
-    const self = this;
-    return self.entries[generateEntryCacheId(options)];
-  }
-
-  /**
-   * Adds a temporary table to the data catalog. This would allow autocomplete etc. of tables that haven't
-   * been created yet.
-   *
-   * Calling this returns a handle that allows deletion of any created entries by calling delete() on the handle.
-   *
-   * @param {Object} options
-   * @param {string} options.name
-   * @param {ContextNamespace} options.namespace - The context namespace
-   * @param {ContextCompute} options.compute - The context compute
-   * @param {Connector} options.connector
-   *
-   * @param {Object[]} options.columns
-   * @param {string} options.columns[].name
-   * @param {string} options.columns[].type
-   * @param {Object[][]} options.sample
-   *
-   * @return {Object}
-   */
-  addTemporaryTable(options) {
-    const self = this;
-    const tableDeferred = $.Deferred();
-    const path = ['default', options.name];
-
-    const identifiersToClean = [];
-
-    const addEntryMeta = function (entry, sourceMeta) {
-      entry.sourceMeta = sourceMeta || entry.definition;
-      entry.sourceMetaPromise = $.Deferred().resolve(entry.sourceMeta).promise();
-      entry.navigatorMeta = { comment: '' };
-      entry.navigatorMetaPromise = $.Deferred().resolve(entry.navigatorMeta).promise();
-      entry.analysis = { is_view: false };
-      entry.analysisPromise = $.Deferred().resolve(entry.analysis).promise();
-    };
-
-    let removeTable = function () {}; // noop until actually added
-
-    const sourceIdentifier = generateEntryCacheId({
-      namespace: options.namespace,
-      path: []
-    });
-
-    if (!self.temporaryEntries[sourceIdentifier]) {
-      const sourceDeferred = $.Deferred();
-      self.temporaryEntries[sourceIdentifier] = sourceDeferred.promise();
-      const sourceEntry = new DataCatalogEntry({
-        isTemporary: true,
-        dataCatalog: self,
-        namespace: options.namespace,
-        compute: options.compute,
-        path: [],
-        definition: {
-          index: 0,
-          optimizerLoaded: true,
-          type: 'source'
-        }
-      });
-      addEntryMeta(sourceEntry);
-      identifiersToClean.push(sourceIdentifier);
-      sourceEntry.childrenPromise = $.Deferred().resolve([]).promise();
-      sourceDeferred.resolve(sourceEntry);
-    }
-
-    self.temporaryEntries[sourceIdentifier].done(sourceEntry => {
-      sourceEntry.getChildren().done(existingTemporaryDatabases => {
-        const databaseIdentifier = generateEntryCacheId({
-          namespace: options.namespace,
-          path: ['default']
-        });
-
-        if (!self.temporaryEntries[databaseIdentifier]) {
-          const databaseDeferred = $.Deferred();
-          self.temporaryEntries[databaseIdentifier] = databaseDeferred.promise();
-          const databaseEntry = new DataCatalogEntry({
-            isTemporary: true,
-            dataCatalog: self,
-            namespace: options.namespace,
-            compute: options.compute,
-            path: ['default'],
-            definition: {
-              index: 0,
-              optimizerLoaded: true,
-              type: 'database'
-            }
-          });
-          addEntryMeta(databaseEntry);
-          identifiersToClean.push(databaseIdentifier);
-          databaseEntry.childrenPromise = $.Deferred().resolve([]).promise();
-          databaseDeferred.resolve(databaseEntry);
-          existingTemporaryDatabases.push(databaseEntry);
-        }
-
-        self.temporaryEntries[databaseIdentifier].done(databaseEntry => {
-          databaseEntry.getChildren().done(existingTemporaryTables => {
-            const tableIdentifier = generateEntryCacheId({
-              namespace: options.namespace,
-              path: path
-            });
-            self.temporaryEntries[tableIdentifier] = tableDeferred.promise();
-            identifiersToClean.push(tableIdentifier);
-
-            const tableEntry = new DataCatalogEntry({
-              isTemporary: true,
-              dataCatalog: self,
-              namespace: options.namespace,
-              compute: options.compute,
-              path: path,
-              definition: {
-                comment: '',
-                index: 0,
-                name: options.name,
-                optimizerLoaded: true,
-                type: 'table'
-              }
-            });
-            existingTemporaryTables.push(tableEntry);
-            const indexToDelete = existingTemporaryTables.length - 1;
-            removeTable = function () {
-              existingTemporaryTables.splice(indexToDelete, 1);
-            };
-
-            const childrenDeferred = $.Deferred();
-            tableEntry.childrenPromise = childrenDeferred.promise();
-
-            if (options.columns) {
-              const childEntries = [];
-
-              addEntryMeta(tableEntry, {
-                columns: [],
-                extended_columns: [],
-                comment: '',
-                notFound: false,
-                is_view: false
-              });
-
-              tableEntry.sample = {
-                data: options.sample || [],
-                meta: tableEntry.sourceMeta.extended_columns
-              };
-              tableEntry.samplePromise = $.Deferred().resolve(tableEntry.sample).promise();
-
-              let index = 0;
-              options.columns.forEach(column => {
-                const columnPath = path.concat(column.name);
-                const columnIdentifier = generateEntryCacheId({
-                  namespace: options.namespace,
-                  path: columnPath
-                });
-
-                const columnDeferred = $.Deferred();
-                self.temporaryEntries[columnIdentifier] = columnDeferred.promise();
-                identifiersToClean.push(columnIdentifier);
-
-                const columnEntry = new DataCatalogEntry({
-                  isTemporary: true,
-                  dataCatalog: self,
-                  namespace: options.namespace,
-                  compute: options.compute,
-                  path: columnPath,
-                  definition: {
-                    comment: '',
-                    index: index++,
-                    name: column.name,
-                    partitionKey: false,
-                    type: column.type
-                  }
-                });
-
-                columnEntry.sample = {
-                  data: [],
-                  meta: column
-                };
-                if (options.sample) {
-                  options.sample.forEach(sampleRow => {
-                    columnEntry.sample.data.push([sampleRow[index - 1]]);
-                  });
-                }
-                columnEntry.samplePromise = $.Deferred().resolve(columnEntry.sample).promise();
-
-                tableEntry.sourceMeta.columns.push(column.name);
-                tableEntry.sourceMeta.extended_columns.push(columnEntry.definition);
-                columnDeferred.resolve(columnEntry);
-                addEntryMeta(columnEntry, {
-                  comment: '',
-                  name: column.name,
-                  notFount: false,
-                  sample: [],
-                  type: column.type
-                });
-
-                childEntries.push(columnEntry);
-              });
-              childrenDeferred.resolve(childEntries);
-            } else {
-              childrenDeferred.resolve([]);
-            }
-
-            tableDeferred.resolve(tableEntry);
-          });
-        });
-      });
-    });
-
-    return {
-      delete: function () {
-        removeTable();
-        while (identifiersToClean.length) {
-          delete self.entries[identifiersToClean.pop()];
-        }
-      }
-    };
-  }
-
-  /**
-   * @param {Object} options
-   * @param {string|string[]} options.path
-   * @param {ContextNamespace} options.namespace - The context namespace
-   * @param {ContextCompute} options.compute - The context compute
-   * @param {Object} [options.definition] - The initial definition if not already set on the entry
-   * @param {boolean} [options.cachedOnly] - Default: false
-   * @param {boolean} [options.temporaryOnly] - Default: false
-   * // @param {Session} [options.session]
-   * @return {Promise}
-   */
-  getEntry(options) {
-    const self = this;
-    const identifier = generateEntryCacheId(options);
-    if (options.temporaryOnly) {
-      return self.temporaryEntries[identifier] || $.Deferred().reject().promise();
-    }
-    if (self.entries[identifier]) {
-      return self.entries[identifier];
-    }
-
-    const deferred = $.Deferred();
-    self.entries[identifier] = deferred.promise();
-
-    if (!cacheEnabled) {
-      deferred.resolve(
-        new DataCatalogEntry({
-          dataCatalog: self,
-          namespace: options.namespace,
-          compute: options.compute,
-          path: options.path,
-          definition: options.definition
-        })
-      );
-    } else {
-      self.store
-        .getItem(identifier)
-        .then(storeEntry => {
-          const definition = storeEntry ? storeEntry.definition : options.definition;
-          const entry = new DataCatalogEntry({
-            dataCatalog: self,
-            namespace: options.namespace,
-            compute: options.compute,
-            path: options.path,
-            definition: definition
-          });
-          if (storeEntry) {
-            mergeEntry(entry, storeEntry);
-          } else if (!options.cachedOnly && options.definition) {
-            entry.saveLater();
-          }
-          deferred.resolve(entry);
-        })
-        .catch(error => {
-          console.warn(error);
-          const entry = new DataCatalogEntry({
-            dataCatalog: self,
-            namespace: options.namespace,
-            compute: options.compute,
-            path: options.path,
-            definition: options.definition
-          });
-          if (!options.cachedOnly && options.definition) {
-            entry.saveLater();
-          }
-          deferred.resolve(entry);
-        });
-    }
-
-    return self.entries[identifier];
-  }
-
-  /**
-   *
-   * @param {Object} options
-   * @param {ContextNamespace} options.namespace - The context namespace
-   * @param {ContextCompute} options.compute - The context compute
-   * @param {Connector} options.connector
-   * @param {string[][]} options.paths
-   *
-   * @return {JQueryPromise}
-   */
-  getMultiTableEntry(options) {
-    const self = this;
-    const identifier = generateEntryCacheId(options);
-    if (self.multiTableEntries[identifier]) {
-      return self.multiTableEntries[identifier];
-    }
-
-    const deferred = $.Deferred();
-    self.multiTableEntries[identifier] = deferred.promise();
-
-    if (!cacheEnabled) {
-      deferred.resolve(
-        new MultiTableEntry({
-          identifier: identifier,
-          dataCatalog: self,
-          paths: options.paths
-        })
-      );
-    } else {
-      self.multiTableStore
-        .getItem(identifier)
-        .then(storeEntry => {
-          const entry = new MultiTableEntry({
-            identifier: identifier,
-            dataCatalog: self,
-            paths: options.paths
-          });
-          if (storeEntry) {
-            mergeMultiTableEntry(entry, storeEntry);
-          }
-          deferred.resolve(entry);
-        })
-        .catch(error => {
-          console.warn(error);
-          deferred.resolve(
-            new MultiTableEntry({
-              identifier: identifier,
-              dataCatalog: self,
-              paths: options.paths
-            })
-          );
-        });
-    }
-
-    return self.multiTableEntries[identifier];
-  }
-
-  /**
-   * Updates the cache for the given multi tableentry
-   *
-   * @param {MultiTableEntry} multiTableEntry
-   * @return {Promise}
-   */
-  persistMultiTableEntry(multiTableEntry) {
-    const self = this;
-    if (
-      !cacheEnabled ||
-      CACHEABLE_TTL.default <= 0 ||
-      CACHEABLE_TTL.optimizer <= 0 ||
-      multiTableEntry.getConnector().optimizer === LOCAL_STRATEGY
-    ) {
-      return $.Deferred().resolve().promise();
-    }
-    const deferred = $.Deferred();
-    self.multiTableStore
-      .setItem(multiTableEntry.identifier, {
-        version: DATA_CATALOG_VERSION,
-        topAggs: multiTableEntry.topAggs,
-        topColumns: multiTableEntry.topColumns,
-        topFilters: multiTableEntry.topFilters,
-        topJoins: multiTableEntry.topJoins
-      })
-      .then(deferred.resolve)
-      .catch(deferred.reject);
-    return deferred.promise();
-  }
-}
-
-const generalDataCatalog = new GeneralDataCatalog();
-const sourceBoundCatalogs = {};
-
-/**
- * Helper function to get the DataCatalog instance for a given data source.
- *
- * @param {Connector} connector
- *
- * @return {DataCatalog}
- */
-const getCatalog = function (connector) {
-  if (!connector || !connector.id) {
-    throw new Error('getCatalog called without connector with id');
-  }
-  return (
-    sourceBoundCatalogs[connector.id] ||
-    (sourceBoundCatalogs[connector.id] = new DataCatalog(connector))
-  );
-};
-
-export default {
-  /**
-   * Adds a detached (temporary) entry to the data catalog. This would allow autocomplete etc. of tables that haven't
-   * been created yet.
-   *
-   * Calling this returns a handle that allows deletion of any created entries by calling delete() on the handle.
-   *
-   * @param {Object} options
-   * @param {ContextNamespace} options.namespace - The context namespace
-   * @param {ContextCompute} options.compute - The context compute
-   * @param {Connector} options.connector
-   * @param {string} options.name
-   *
-   * @param {Object[]} options.columns
-   * @param {string} options.columns[].name
-   * @param {string} options.columns[].type
-   * @param {Object[][]} options.sample
-   *
-   * @return {Object}
-   */
-  addTemporaryTable: function (options) {
-    return getCatalog(options.connector).addTemporaryTable(options);
-  },
-
-  /**
-   * @param {Object} options
-   * @param {ContextNamespace} options.namespace - The context namespace
-   * @param {ContextCompute} options.compute - The context compute
-   * @param {Connector} options.connector
-   * @param {string|string[]} options.path
-   * @param {Object} [options.definition] - Optional initial definition
-   * @param {boolean} [options.cachedOnly] - Default: false
-   * @param {boolean} [options.temporaryOnly] - Default: false
-   *
-   * @return {JQuery.Promise<DataCatalogEntry>}
-   */
-  getEntry: function (options) {
-    return getCatalog(options.connector).getEntry(options);
-  },
-
-  /**
-   * @param {Object} options
-   * @param {ContextNamespace} options.namespace - The context namespace
-   * @param {ContextCompute} options.compute - The context compute
-   * @param {Connector} options.connector
-   * @param {string[][]} options.paths
-   *
-   * @return {JQueryPromise}
-   */
-  getMultiTableEntry: function (options) {
-    return getCatalog(options.connector).getMultiTableEntry(options);
-  },
-
-  /**
-   * This can be used as a shorthand function to get the child entries of the given path. Same as first calling
-   * getEntry then getChildren.
-   *
-   * @param {Object} options
-   * @param {Namespace} options.namespace - The context namespace
-   * @param {Compute} options.compute - The context compute
-   * @param {Connector} options.connector
-   * @param {string|string[]} options.path
-   * @param {Object} [options.definition] - Optional initial definition of the parent entry
-   * @param {boolean} [options.silenceErrors]
-   * @param {boolean} [options.cachedOnly]
-   * @param {boolean} [options.temporaryOnly]
-   * @param {boolean} [options.refreshCache]
-   * @param {boolean} [options.cancellable] - Default false
-   *
-   * @return {CancellableJqPromise}
-   */
-  getChildren: function (options) {
-    const deferred = $.Deferred();
-    const cancellablePromises = [];
-    getCatalog(options.connector)
-      .getEntry(options)
-      .done(entry => {
-        cancellablePromises.push(
-          entry.getChildren(options).done(deferred.resolve).fail(deferred.reject)
-        );
-      })
-      .fail(deferred.reject);
-    return new CancellableJqPromise(deferred, undefined, cancellablePromises);
-  },
-
-  /**
-   * @param {Connector} connector
-   *
-   * @return {DataCatalog}
-   */
-  getCatalog: getCatalog,
-
-  /**
-   * @param {Object} [options]
-   * @param {boolean} [options.silenceErrors]
-   * @param {boolean} [options.refreshCache]
-   *
-   * @return {Promise}
-   */
-  getAllNavigatorTags: generalDataCatalog.getAllNavigatorTags.bind(generalDataCatalog),
-
-  /**
-   * @param {string[]} tagsToAdd
-   * @param {string[]} tagsToRemove
-   */
-  updateAllNavigatorTags: generalDataCatalog.updateAllNavigatorTags.bind(generalDataCatalog),
-
-  enableCache: function () {
-    cacheEnabled = true;
-  },
-
-  disableCache: function () {
-    cacheEnabled = false;
-  },
-
-  applyCancellable: catalogUtils.applyCancellable
-};

+ 880 - 0
desktop/core/src/desktop/js/catalog/dataCatalog.ts

@@ -0,0 +1,880 @@
+// 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.
+
+import localforage from 'localforage';
+
+import { Cancellable, CancellablePromise } from 'api/cancellablePromise';
+import { applyCancellable } from 'catalog/catalogUtils';
+import DataCatalogEntry, {
+  Analysis,
+  Definition,
+  ExtendedColumn,
+  FieldSample,
+  NavigatorMeta,
+  OptimizerMeta,
+  Partitions,
+  Sample,
+  SourceMeta,
+  TableSourceMeta
+} from 'catalog/DataCatalogEntry';
+import GeneralDataCatalog from 'catalog/GeneralDataCatalog';
+import MultiTableEntry, {
+  TopAggs,
+  TopColumns,
+  TopFilters,
+  TopJoins
+} from 'catalog/MultiTableEntry';
+import { getOptimizer, LOCAL_STRATEGY } from 'catalog/optimizer/optimizer';
+import { Compute, Connector, Namespace } from 'types/config';
+import { hueWindow } from 'types/types';
+
+export interface TimestampedData {
+  hueTimestamp?: number;
+}
+
+export interface CatalogGetOptions {
+  silenceErrors?: boolean;
+  cachedOnly?: boolean;
+  refreshCache?: boolean;
+  cancellable?: boolean;
+}
+
+export interface GetEntryOptions {
+  path: string | string[];
+  namespace: Namespace;
+  compute: Compute;
+  definition?: Definition;
+  cachedOnly?: boolean;
+  temporaryOnly?: boolean;
+}
+
+interface GetMultiTableEntryOptions {
+  namespace: Namespace;
+  compute: Compute;
+  paths: string[][];
+}
+
+interface AddTemporaryTableOptions {
+  name: string;
+  namespace: Namespace;
+  compute: Compute;
+  database?: string;
+  columns: { name: string; type: string }[];
+  sample: FieldSample[][];
+}
+
+interface StoreEntry {
+  version: number;
+  definition?: Definition;
+  sourceMeta?: SourceMeta;
+  analysis?: Analysis;
+  partitions?: Partitions;
+  sample?: Sample;
+  navigatorMeta?: NavigatorMeta;
+  optimizerMeta?: OptimizerMeta;
+  optimizerPopularity?: OptimizerPopularity;
+}
+
+interface StoreMultiTableEntry {
+  version: number;
+  topAggs?: TopAggs;
+  topJoins?: TopJoins;
+  topColumns?: TopColumns;
+  topFilters?: TopFilters;
+}
+
+export interface OptimizerResponsePopularity {
+  name?: string;
+  columnCount: number;
+  dbName?: string;
+  tableName?: string;
+  columnName?: string;
+}
+
+export interface OptimizerResponseValues {
+  filterColumns?: OptimizerResponsePopularity[];
+  groupbyColumns?: OptimizerResponsePopularity[];
+  joinColumns?: OptimizerResponsePopularity[];
+  orderbyColumns?: OptimizerResponsePopularity[];
+  selectColumns?: OptimizerResponsePopularity[];
+}
+
+export interface OptimizerResponse {
+  top_tables?: OptimizerResponsePopularity[];
+  values?: OptimizerResponseValues;
+}
+
+export interface OptimizerPopularitySubType {
+  filterColumn?: OptimizerResponsePopularity;
+  groupByColumn?: OptimizerResponsePopularity;
+  joinColumn?: OptimizerResponsePopularity;
+  orderByColumn?: OptimizerResponsePopularity;
+  selectColumn?: OptimizerResponsePopularity;
+}
+
+export interface OptimizerPopularity
+  extends TimestampedData,
+    OptimizerResponsePopularity,
+    OptimizerPopularitySubType {
+  column_count?: number;
+  popularity?: number;
+  relativePopularity?: number;
+}
+
+const STORAGE_POSTFIX = (<hueWindow>window).LOGGED_USERNAME || '';
+const DATA_CATALOG_VERSION = 5;
+
+let cacheEnabled = true;
+
+/**
+ * Creates a cache identifier given a namespace and path(s)
+ */
+const generateEntryCacheId = (options: {
+  path?: string | string[];
+  paths?: string[][];
+  namespace: Namespace;
+}): string => {
+  let id = options.namespace.id;
+  if (options.path) {
+    if (typeof options.path === 'string') {
+      id += '_' + options.path;
+    } else if (options.path.length) {
+      id += '_' + options.path.join('.');
+    }
+  } else if (options.paths && options.paths.length) {
+    const pathSet: { [path: string]: boolean } = {};
+    options.paths.forEach(path => {
+      pathSet[path.join('.')] = true;
+    });
+    const uniquePaths = Object.keys(pathSet);
+    uniquePaths.sort();
+    id += '_' + uniquePaths.join(',');
+  }
+  return id;
+};
+
+const isFresh = (storeEntryValue: TimestampedData, ttl?: number) => {
+  const confTtl = (<hueWindow>window).CACHEABLE_TTL || {};
+  const ttlToCheck = typeof ttl !== 'undefined' ? ttl : confTtl.default;
+  if (!storeEntryValue.hueTimestamp || typeof ttlToCheck === 'undefined') {
+    return true;
+  }
+  return Date.now() - storeEntryValue.hueTimestamp < ttlToCheck;
+};
+
+/**
+ * Helper function to fill a catalog entry with cached metadata.
+ */
+const mergeEntry = (dataCatalogEntry: DataCatalogEntry, storeEntry: StoreEntry): void => {
+  if (storeEntry.version !== DATA_CATALOG_VERSION) {
+    return;
+  }
+
+  if (storeEntry.definition && isFresh(storeEntry.definition)) {
+    dataCatalogEntry.definition = storeEntry.definition;
+  }
+  if (storeEntry.sourceMeta && isFresh(storeEntry.sourceMeta)) {
+    dataCatalogEntry.sourceMeta = storeEntry.sourceMeta;
+    dataCatalogEntry.sourceMetaPromise = CancellablePromise.resolve(dataCatalogEntry.sourceMeta);
+  }
+  if (storeEntry.analysis && isFresh(storeEntry.analysis)) {
+    dataCatalogEntry.analysis = storeEntry.analysis;
+    dataCatalogEntry.analysisPromise = CancellablePromise.resolve(dataCatalogEntry.analysis);
+  }
+  if (storeEntry.partitions && isFresh(storeEntry.partitions)) {
+    dataCatalogEntry.partitions = storeEntry.partitions;
+    dataCatalogEntry.partitionsPromise = CancellablePromise.resolve(dataCatalogEntry.partitions);
+  }
+  if (storeEntry.sample && isFresh(storeEntry.sample)) {
+    dataCatalogEntry.sample = storeEntry.sample;
+    dataCatalogEntry.samplePromise = CancellablePromise.resolve(dataCatalogEntry.sample);
+  }
+  if (storeEntry.navigatorMeta && isFresh(storeEntry.navigatorMeta)) {
+    dataCatalogEntry.navigatorMeta = storeEntry.navigatorMeta;
+    dataCatalogEntry.navigatorMetaPromise = CancellablePromise.resolve(
+      dataCatalogEntry.navigatorMeta
+    );
+  }
+  if (dataCatalogEntry.getConnector().optimizer !== LOCAL_STRATEGY) {
+    const confTtl = (<hueWindow>window).CACHEABLE_TTL || {};
+    if (storeEntry.optimizerMeta && isFresh(storeEntry.optimizerMeta, confTtl.optimizer)) {
+      dataCatalogEntry.optimizerMeta = storeEntry.optimizerMeta;
+      dataCatalogEntry.optimizerMetaPromise = CancellablePromise.resolve(
+        dataCatalogEntry.optimizerMeta
+      );
+    }
+    if (
+      storeEntry.optimizerPopularity &&
+      isFresh(storeEntry.optimizerPopularity, confTtl.optimizer)
+    ) {
+      dataCatalogEntry.optimizerPopularity = storeEntry.optimizerPopularity;
+    }
+  }
+};
+
+/**
+ * Helper function to fill a multi table catalog entry with cached metadata.
+ */
+const mergeMultiTableEntry = (
+  multiTableEntry: MultiTableEntry,
+  storeEntry: StoreMultiTableEntry
+): void => {
+  if (
+    multiTableEntry.getConnector().optimizer === LOCAL_STRATEGY ||
+    storeEntry.version !== DATA_CATALOG_VERSION
+  ) {
+    return;
+  }
+  const confTtl = (<hueWindow>window).CACHEABLE_TTL || {};
+  if (storeEntry.topAggs && isFresh(storeEntry.topAggs, confTtl.optimizer)) {
+    multiTableEntry.topAggs = storeEntry.topAggs;
+    multiTableEntry.topAggsPromise = CancellablePromise.resolve(multiTableEntry.topAggs);
+  }
+  if (storeEntry.topColumns && isFresh(storeEntry.topColumns, confTtl.optimizer)) {
+    multiTableEntry.topColumns = storeEntry.topColumns;
+    multiTableEntry.topColumnsPromise = CancellablePromise.resolve(multiTableEntry.topColumns);
+  }
+  if (storeEntry.topFilters && isFresh(storeEntry.topFilters, confTtl.optimizer)) {
+    multiTableEntry.topFilters = storeEntry.topFilters;
+    multiTableEntry.topFiltersPromise = CancellablePromise.resolve(multiTableEntry.topFilters);
+  }
+  if (storeEntry.topJoins && isFresh(storeEntry.topJoins, confTtl.optimizer)) {
+    multiTableEntry.topJoins = storeEntry.topJoins;
+    multiTableEntry.topJoinsPromise = CancellablePromise.resolve(multiTableEntry.topJoins);
+  }
+};
+
+export class DataCatalog {
+  connector: Connector;
+  entries: { [key: string]: Promise<DataCatalogEntry> } = {};
+  temporaryEntries: { [key: string]: Promise<DataCatalogEntry> } = {};
+  multiTableEntries: { [key: string]: Promise<MultiTableEntry> } = {};
+  store: LocalForage;
+  multiTableStore: LocalForage;
+
+  invalidatePromise?: Promise<void>;
+
+  constructor(connector: Connector) {
+    if (!connector || !connector.id) {
+      throw new Error('DataCatalog created without connector or id');
+    }
+    this.connector = connector;
+
+    this.store = localforage.createInstance({
+      name: 'HueDataCatalog_' + this.connector.id + '_' + STORAGE_POSTFIX
+    });
+    this.multiTableStore = localforage.createInstance({
+      name: 'HueDataCatalog_' + this.connector.id + '_multiTable_' + STORAGE_POSTFIX
+    });
+  }
+
+  /**
+   * Disables the caching for subsequent operations, mainly used for test purposes
+   */
+  static disableCache(): void {
+    cacheEnabled = false;
+  }
+
+  /**
+   * Enables the cache for subsequent operations, mainly used for test purposes
+   */
+  static enableCache(): void {
+    cacheEnabled = true;
+  }
+
+  static cacheEnabled(): boolean {
+    return cacheEnabled;
+  }
+
+  /**
+   * Returns true if the catalog can have Optimizer metadata
+   */
+  canHaveOptimizerMeta(): boolean {
+    return !!(
+      (<hueWindow>window).HAS_OPTIMIZER &&
+      this.connector &&
+      this.connector.optimizer &&
+      this.connector.optimizer !== 'off'
+    );
+  }
+
+  /**
+   * Clears the data catalog and cache for the given path and any children thereof.
+   */
+  async clearStorageCascade(
+    namespace: Namespace,
+    compute: Compute,
+    pathToClear: string[]
+  ): Promise<void> {
+    if (!namespace || !compute) {
+      if (pathToClear.length === 0) {
+        this.entries = {};
+        return this.store.clear();
+      }
+      return;
+    }
+
+    const keyPrefix = generateEntryCacheId({ namespace: namespace, path: pathToClear });
+    Object.keys(this.entries).forEach(key => {
+      if (key.indexOf(keyPrefix) === 0) {
+        delete this.entries[key];
+      }
+    });
+
+    const deletePromises: Promise<void>[] = [];
+    try {
+      const keys = await this.store.keys();
+      keys.forEach(key => {
+        if (key.indexOf(keyPrefix) === 0) {
+          deletePromises.push(this.store.removeItem(key));
+        }
+      });
+      await Promise.all(deletePromises);
+    } catch (err) {}
+  }
+
+  /**
+   * Updates the cache for the given entry
+   */
+  async persistCatalogEntry(dataCatalogEntry: DataCatalogEntry): Promise<void> {
+    const confTtl = (<hueWindow>window).CACHEABLE_TTL || {};
+    if (!cacheEnabled || !confTtl.default || confTtl.default <= 0) {
+      return;
+    }
+    const identifier = generateEntryCacheId(dataCatalogEntry);
+
+    await this.store.setItem<StoreEntry>(identifier, {
+      version: DATA_CATALOG_VERSION,
+      definition: dataCatalogEntry.definition,
+      sourceMeta: dataCatalogEntry.sourceMeta,
+      analysis: dataCatalogEntry.analysis,
+      partitions: dataCatalogEntry.partitions,
+      sample: dataCatalogEntry.sample,
+      navigatorMeta: dataCatalogEntry.navigatorMeta,
+      optimizerMeta:
+        this.connector.optimizer !== LOCAL_STRATEGY ? dataCatalogEntry.optimizerMeta : undefined,
+      optimizerPopularity:
+        this.connector.optimizer !== LOCAL_STRATEGY
+          ? dataCatalogEntry.optimizerPopularity
+          : undefined
+    });
+  }
+
+  /**
+   * Loads Navigator Optimizer popularity for multiple tables in one go.
+   */
+  loadOptimizerPopularityForTables(options: {
+    namespace: Namespace;
+    compute: Compute;
+    paths: string[][];
+    silenceErrors?: boolean;
+    cancellable?: boolean;
+  }): CancellablePromise<DataCatalogEntry[]> {
+    const cancellablePromises: Cancellable[] = [];
+    const popularEntries: DataCatalogEntry[] = [];
+    const pathsToLoad: string[][] = [];
+
+    const existingPromises: Promise<void>[] = [];
+    options.paths.forEach(path => {
+      const existingPromise = new Promise<void>(async (resolve, reject) => {
+        try {
+          const tableEntry = await this.getEntry({
+            namespace: options.namespace,
+            compute: options.compute,
+            path: path
+          });
+          if (tableEntry.optimizerPopularityForChildrenPromise) {
+            const existingPopularEntries = await tableEntry.optimizerPopularityForChildrenPromise;
+            popularEntries.push(...existingPopularEntries);
+            resolve();
+          } else if (tableEntry.definition && tableEntry.definition.optimizerLoaded) {
+            const childPromise = tableEntry.getChildren({ ...options, silenceErrors: true });
+            cancellablePromises.push(childPromise);
+            const childEntries = await childPromise;
+            childEntries.forEach(childEntry => {
+              if (childEntry.optimizerPopularity) {
+                popularEntries.push(childEntry);
+              }
+            });
+            resolve();
+          } else {
+            pathsToLoad.push(path);
+          }
+        } catch (err) {
+          reject(err);
+        }
+      });
+      existingPromises.push(existingPromise);
+    });
+
+    const popularityPromise = new CancellablePromise<DataCatalogEntry[]>(
+      async (resolve, reject, onCancel) => {
+        onCancel(() => {
+          cancellablePromises.forEach(cancellable => {
+            cancellable.cancel();
+          });
+        });
+        try {
+          await Promise.all(existingPromises);
+        } catch (err) {}
+
+        if (!pathsToLoad.length) {
+          resolve(popularEntries);
+          return;
+        }
+
+        const fetchPromise = getOptimizer(this.connector).fetchPopularity({
+          silenceErrors: true,
+          paths: pathsToLoad
+        });
+        cancellablePromises.push(fetchPromise);
+
+        fetchPromise
+          .done((data: OptimizerResponse) => {
+            const perTable: { [path: string]: OptimizerResponse } = {};
+
+            const splitOptimizerValuesPerTable = (
+              listName: keyof OptimizerResponseValues
+            ): void => {
+              const values = data.values && data.values[listName];
+              if (values) {
+                values.forEach(column => {
+                  let tableMeta = perTable[column.dbName + '.' + column.tableName];
+                  if (!tableMeta) {
+                    tableMeta = { values: {} };
+                    perTable[column.dbName + '.' + column.tableName] = tableMeta;
+                  }
+                  if (tableMeta.values) {
+                    let valuesList = tableMeta.values[listName];
+                    if (!valuesList) {
+                      valuesList = [];
+                      tableMeta.values[listName] = valuesList;
+                    }
+                    valuesList.push(column);
+                  }
+                });
+              }
+            };
+
+            if (data.values) {
+              splitOptimizerValuesPerTable('filterColumns');
+              splitOptimizerValuesPerTable('groupbyColumns');
+              splitOptimizerValuesPerTable('joinColumns');
+              splitOptimizerValuesPerTable('orderbyColumns');
+              splitOptimizerValuesPerTable('selectColumns');
+            }
+
+            const tablePromises: Promise<void>[] = Object.keys(perTable).map(
+              path =>
+                new Promise<void>(async resolve => {
+                  try {
+                    const entry = await this.getEntry({
+                      namespace: options.namespace,
+                      compute: options.compute,
+                      path: path
+                    });
+                    const applyPromise = entry.applyOptimizerResponseToChildren(perTable[path], {
+                      ...options,
+                      silenceErrors: true
+                    });
+                    cancellablePromises.push(applyPromise);
+                    popularEntries.push(...(await applyPromise));
+                  } catch (err) {}
+                  resolve();
+                })
+            );
+
+            Promise.all(tablePromises).finally(() => {
+              resolve(popularEntries);
+            });
+          })
+          .fail(() => {
+            resolve(popularEntries);
+          });
+      }
+    );
+    return applyCancellable(popularityPromise);
+  }
+
+  async getKnownEntry(options: {
+    namespace: Namespace;
+    compute: Compute;
+    path: string | string[];
+  }): Promise<DataCatalogEntry> {
+    return this.entries[generateEntryCacheId(options)];
+  }
+
+  /**
+   * Adds a temporary table to the data catalog. This would allow autocomplete etc. of tables that haven't
+   * been created yet.
+   *
+   * Calling this returns a handle that allows deletion of any created entries by calling delete() on the handle.
+   */
+  addTemporaryTable(options: AddTemporaryTableOptions): { delete: () => void } {
+    const database = options.database || 'default';
+    const path = [database, options.name];
+
+    const identifiersToClean: string[] = [];
+
+    const addEntryMeta = (entry: DataCatalogEntry, sourceMeta?: SourceMeta) => {
+      entry.sourceMeta = sourceMeta || <SourceMeta>entry.definition;
+      entry.sourceMetaPromise = CancellablePromise.resolve(entry.sourceMeta);
+    };
+
+    const sourceIdentifier = generateEntryCacheId({
+      namespace: options.namespace,
+      path: []
+    });
+
+    // Create the source entry if not already present
+    if (!this.temporaryEntries[sourceIdentifier]) {
+      const sourceEntry = new DataCatalogEntry({
+        isTemporary: true,
+        dataCatalog: this,
+        namespace: options.namespace,
+        compute: options.compute,
+        path: [],
+        definition: {
+          index: 0,
+          optimizerLoaded: true,
+          type: 'source'
+        }
+      });
+      addEntryMeta(sourceEntry);
+      identifiersToClean.push(sourceIdentifier);
+      sourceEntry.childrenPromise = CancellablePromise.resolve([]);
+      this.temporaryEntries[sourceIdentifier] = Promise.resolve(sourceEntry);
+    }
+
+    this.temporaryEntries[sourceIdentifier].then(async sourceEntry => {
+      const existingTemporaryDatabases = await sourceEntry.getChildren();
+      const databaseIdentifier = generateEntryCacheId({
+        namespace: options.namespace,
+        path: [database]
+      });
+
+      // Create the database entry if not already present
+      if (!this.temporaryEntries[databaseIdentifier]) {
+        const databaseEntry = new DataCatalogEntry({
+          isTemporary: true,
+          dataCatalog: this,
+          namespace: options.namespace,
+          compute: options.compute,
+          path: [database],
+          definition: {
+            index: 0,
+            optimizerLoaded: true,
+            type: 'database'
+          }
+        });
+        addEntryMeta(databaseEntry);
+        databaseEntry.childrenPromise = CancellablePromise.resolve([]);
+        identifiersToClean.push(databaseIdentifier);
+        existingTemporaryDatabases.push(databaseEntry);
+        this.temporaryEntries[databaseIdentifier] = Promise.resolve(databaseEntry);
+      }
+
+      const databaseEntry = await this.temporaryEntries[databaseIdentifier];
+      const existingTemporaryTables = await databaseEntry.getChildren();
+
+      const tableIdentifier = generateEntryCacheId({
+        namespace: options.namespace,
+        path: path
+      });
+
+      // Unlink any existing table with the same identifier
+      if (this.temporaryEntries[tableIdentifier]) {
+        const tableEntry = await this.temporaryEntries[tableIdentifier];
+        const index = existingTemporaryTables.indexOf(tableEntry);
+        if (index !== -1) {
+          existingTemporaryTables.splice(index, 1);
+        }
+      }
+
+      const tableEntry = new DataCatalogEntry({
+        isTemporary: true,
+        dataCatalog: this,
+        namespace: options.namespace,
+        compute: options.compute,
+        path: path,
+        definition: {
+          comment: '',
+          index: existingTemporaryTables.length,
+          name: options.name,
+          optimizerLoaded: true,
+          type: 'table'
+        }
+      });
+
+      existingTemporaryTables.push(tableEntry);
+
+      const tableSourceMeta: TableSourceMeta = {
+        columns: (options.columns || []).map(col => col.name),
+        extended_columns: options.columns || [],
+        comment: '',
+        notFound: false,
+        is_view: false
+      };
+
+      addEntryMeta(tableEntry, tableSourceMeta);
+
+      tableEntry.sample = { data: options.sample, meta: options.columns, type: 'table' };
+      tableEntry.samplePromise = CancellablePromise.resolve(tableEntry.sample);
+
+      identifiersToClean.push(tableIdentifier);
+      this.temporaryEntries[tableIdentifier] = Promise.resolve(tableEntry);
+
+      const columnEntries: DataCatalogEntry[] = [];
+      tableEntry.childrenPromise = CancellablePromise.resolve(columnEntries);
+
+      options.columns.forEach((column, index) => {
+        const columnPath = [...path, column.name];
+        const columnEntry = new DataCatalogEntry({
+          isTemporary: true,
+          dataCatalog: this,
+          namespace: options.namespace,
+          compute: options.compute,
+          path: columnPath,
+          definition: {
+            comment: '',
+            index: index,
+            name: column.name,
+            partitionKey: false,
+            type: column.type
+          }
+        });
+
+        columnEntry.sample = {
+          data: options.sample.map(sampleRow => [sampleRow[index]]),
+          meta: [column],
+          type: 'table'
+        };
+        columnEntry.samplePromise = CancellablePromise.resolve(columnEntry.sample);
+
+        tableSourceMeta.columns.push(column.name);
+        tableSourceMeta.extended_columns.push(<ExtendedColumn>columnEntry.definition);
+        addEntryMeta(columnEntry, {
+          comment: '',
+          name: column.name,
+          notFound: false,
+          sample: columnEntry.sample.data,
+          type: column.type
+        });
+
+        const columnIdentifier = generateEntryCacheId({
+          namespace: options.namespace,
+          path: columnPath
+        });
+        identifiersToClean.push(columnIdentifier);
+        this.temporaryEntries[columnIdentifier] = CancellablePromise.resolve(columnEntry);
+      });
+    });
+
+    return {
+      delete: () => {
+        while (identifiersToClean.length) {
+          const nextToDelete = identifiersToClean.pop();
+          if (nextToDelete) {
+            delete this.temporaryEntries[nextToDelete];
+          }
+        }
+      }
+    };
+  }
+
+  async getEntry(options: GetEntryOptions): Promise<DataCatalogEntry> {
+    const identifier = generateEntryCacheId(options);
+    if (options.temporaryOnly) {
+      return this.temporaryEntries[identifier] || $.Deferred().reject().promise();
+    }
+    if (this.entries[identifier]) {
+      return this.entries[identifier];
+    }
+
+    this.entries[identifier] = new Promise<DataCatalogEntry>(resolve => {
+      if (!cacheEnabled) {
+        resolve(
+          new DataCatalogEntry({
+            dataCatalog: this,
+            namespace: options.namespace,
+            compute: options.compute,
+            path: options.path,
+            definition: options.definition
+          })
+        );
+      } else {
+        this.store
+          .getItem<StoreEntry>(identifier)
+          .then(storeEntry => {
+            const definition = storeEntry ? storeEntry.definition : options.definition;
+            const entry = new DataCatalogEntry({
+              dataCatalog: this,
+              namespace: options.namespace,
+              compute: options.compute,
+              path: options.path,
+              definition: definition
+            });
+            if (storeEntry) {
+              mergeEntry(entry, storeEntry);
+            } else if (!options.cachedOnly && options.definition) {
+              entry.saveLater();
+            }
+            resolve(entry);
+          })
+          .catch(error => {
+            console.warn(error);
+            const entry = new DataCatalogEntry({
+              dataCatalog: this,
+              namespace: options.namespace,
+              compute: options.compute,
+              path: options.path,
+              definition: options.definition
+            });
+            if (!options.cachedOnly && options.definition) {
+              entry.saveLater();
+            }
+            resolve(entry);
+          });
+      }
+    });
+
+    return this.entries[identifier];
+  }
+
+  async getMultiTableEntry(options: GetMultiTableEntryOptions): Promise<MultiTableEntry> {
+    const identifier = generateEntryCacheId(options);
+    if (this.multiTableEntries[identifier]) {
+      return this.multiTableEntries[identifier];
+    }
+
+    const newEntry = new MultiTableEntry({
+      identifier: identifier,
+      dataCatalog: this,
+      paths: options.paths
+    });
+
+    this.multiTableEntries[identifier] = new Promise<MultiTableEntry>(async resolve => {
+      if (!cacheEnabled) {
+        resolve(newEntry);
+        return;
+      }
+      try {
+        const storeEntry = await this.multiTableStore.getItem<StoreMultiTableEntry>(identifier);
+        mergeMultiTableEntry(newEntry, storeEntry);
+      } catch (err) {
+        console.warn(err);
+      }
+      resolve(newEntry);
+    });
+
+    return this.multiTableEntries[identifier];
+  }
+
+  /**
+   * Updates the cache for the given multi table entry
+   */
+  async persistMultiTableEntry(multiTableEntry: MultiTableEntry): Promise<void> {
+    const confTtl = (<hueWindow>window).CACHEABLE_TTL || {};
+    if (
+      !cacheEnabled ||
+      (confTtl.default && confTtl.default <= 0) ||
+      (confTtl.optimizer && confTtl.optimizer <= 0) ||
+      multiTableEntry.getConnector().optimizer === LOCAL_STRATEGY
+    ) {
+      return;
+    }
+    await this.multiTableStore.setItem<StoreMultiTableEntry>(multiTableEntry.identifier, {
+      version: DATA_CATALOG_VERSION,
+      topAggs: multiTableEntry.topAggs,
+      topColumns: multiTableEntry.topColumns,
+      topFilters: multiTableEntry.topFilters,
+      topJoins: multiTableEntry.topJoins
+    });
+  }
+}
+
+const generalDataCatalog = new GeneralDataCatalog();
+const sourceBoundCatalogs: { [connectorId: string]: DataCatalog } = {};
+
+/**
+ * Helper function to get the DataCatalog instance for a given data source.
+ */
+const getCatalog = (connector: Connector): DataCatalog => {
+  if (!connector || !connector.id) {
+    throw new Error('getCatalog called without connector with id');
+  }
+  return (
+    sourceBoundCatalogs[connector.id] ||
+    (sourceBoundCatalogs[connector.id] = new DataCatalog(connector))
+  );
+};
+
+export default {
+  /**
+   * Adds a detached (temporary) entry to the data catalog. This would allow autocomplete etc. of tables that haven't
+   * been created yet.
+   *
+   * Calling this returns a handle that allows deletion of any created entries by calling delete() on the handle.
+   */
+  addTemporaryTable: (
+    options: { connector: Connector } & AddTemporaryTableOptions
+  ): { delete: () => void } => getCatalog(options.connector).addTemporaryTable(options),
+
+  getEntry: (options: { connector: Connector } & GetEntryOptions): Promise<DataCatalogEntry> =>
+    getCatalog(options.connector).getEntry(options),
+
+  getMultiTableEntry: (
+    options: { connector: Connector } & GetMultiTableEntryOptions
+  ): Promise<MultiTableEntry> => getCatalog(options.connector).getMultiTableEntry(options),
+
+  /**
+   * This can be used as a shorthand function to get the child entries of the given path. Same as first calling
+   * getEntry then getChildren.
+   */
+  getChildren: (
+    options: {
+      connector: Connector;
+      namespace: Namespace;
+      compute: Compute;
+      path: string | string[];
+      temporaryOnly?: boolean;
+    } & CatalogGetOptions
+  ): CancellablePromise<DataCatalogEntry[]> =>
+    new CancellablePromise<DataCatalogEntry[]>(async (resolve, reject, onCancel) => {
+      const entry = await getCatalog(options.connector).getEntry(options);
+
+      const childPromise = entry.getChildren(options);
+      onCancel(() => {
+        childPromise.cancel();
+      });
+
+      return applyCancellable(childPromise, options);
+    }),
+
+  getCatalog,
+
+  getAllNavigatorTags: generalDataCatalog.getAllNavigatorTags.bind(generalDataCatalog),
+
+  updateAllNavigatorTags: generalDataCatalog.updateAllNavigatorTags.bind(generalDataCatalog),
+
+  enableCache(): void {
+    cacheEnabled = true;
+  },
+
+  disableCache(): void {
+    cacheEnabled = false;
+  }
+};

+ 0 - 68
desktop/core/src/desktop/js/catalog/dataCatalog/index.d.ts

@@ -1,68 +0,0 @@
-// 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.
-
-import CancellableJqPromise from 'api/cancellableJqPromise';
-import DataCatalogEntry from 'catalog/dataCatalogEntry';
-import { Compute, Connector, Namespace } from 'types/config';
-
-interface BaseOptions {
-  namespace: Namespace;
-  compute: Compute;
-  definition?: unknown;
-  silenceErrors?: boolean;
-  cachedOnly?: boolean;
-  refreshCache?: boolean;
-  cancellable?: boolean;
-  temporaryOnly?: boolean;
-}
-
-interface SingleEntryOptions extends BaseOptions {
-  path: string | string[];
-}
-
-interface MultiEntryOptions extends BaseOptions {
-  paths: (string | string[])[];
-}
-
-interface StaticSingleEntryOptions extends SingleEntryOptions {
-  connector: Connector;
-}
-
-interface StaticMultiEntryOptions extends MultiEntryOptions {
-  connector: Connector;
-}
-
-interface DataCatalog {
-  getChildren(options: SingleEntryOptions): CancellableJqPromise<DataCatalogEntry[]>;
-  getEntry(options: SingleEntryOptions): CancellableJqPromise<DataCatalogEntry>;
-  getMultiTableEntry(options: MultiEntryOptions): CancellableJqPromise<DataCatalogEntry>;
-  loadOptimizerPopularityForTables(
-    options: MultiEntryOptions
-  ): CancellableJqPromise<DataCatalogEntry[]>;
-}
-
-declare const dataCatalog: {
-  getChildren(options: StaticSingleEntryOptions): CancellableJqPromise<DataCatalogEntry[]>;
-  getEntry(options: StaticSingleEntryOptions): CancellableJqPromise<DataCatalogEntry>;
-  getMultiTableEntry(options: StaticMultiEntryOptions): CancellableJqPromise<DataCatalogEntry>;
-  loadOptimizerPopularityForTables(
-    options: StaticMultiEntryOptions
-  ): CancellableJqPromise<DataCatalogEntry[]>;
-  getCatalog(connector: Connector): DataCatalog;
-  applyCancellable(promise: CancellableJqPromise<unknown>, options: { cancellable: boolean }): void;
-};
-
-export = dataCatalog;

+ 0 - 1840
desktop/core/src/desktop/js/catalog/dataCatalogEntry.js

@@ -1,1840 +0,0 @@
-// 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.
-
-import $ from 'jquery';
-import * as ko from 'knockout';
-
-import apiHelper from 'api/apiHelper';
-import CancellableJqPromise from 'api/cancellableJqPromise';
-import catalogUtils from 'catalog/catalogUtils';
-import huePubSub from 'utils/huePubSub';
-import I18n from 'utils/i18n';
-import { getOptimizer } from './optimizer/optimizer';
-import { DataCatalog } from './dataCatalog';
-
-/**
- * Helper function to reload the source meta for the given entry
- *
- * @param {DataCatalogEntry} dataCatalogEntry
- * @param {Object} [options]
- * @param {boolean} [options.silenceErrors]
- *
- * @return {CancellableJqPromise}
- */
-const reloadSourceMeta = function (dataCatalogEntry, options) {
-  if (dataCatalogEntry.dataCatalog.invalidatePromise) {
-    const deferred = $.Deferred();
-    const cancellablePromises = [];
-    dataCatalogEntry.dataCatalog.invalidatePromise.always(() => {
-      cancellablePromises.push(
-        catalogUtils
-          .fetchAndSave(
-            apiHelper.fetchSourceMetadata.bind(apiHelper),
-            val => {
-              dataCatalogEntry.sourceMeta = val;
-            },
-            dataCatalogEntry,
-            options
-          )
-          .done(deferred.resolve)
-          .fail(deferred.reject)
-      );
-    });
-    return dataCatalogEntry.trackedPromise(promise => {
-      dataCatalogEntry.sourceMetaPromise = promise;
-    }, new CancellableJqPromise(deferred, undefined, cancellablePromises));
-  }
-
-  return dataCatalogEntry.trackedPromise(
-    promise => {
-      dataCatalogEntry.sourceMetaPromise = promise;
-    },
-    catalogUtils.fetchAndSave(
-      apiHelper.fetchSourceMetadata.bind(apiHelper),
-      val => {
-        dataCatalogEntry.sourceMeta = val;
-      },
-      dataCatalogEntry,
-      options
-    )
-  );
-};
-
-/**
- * Helper function to reload the navigator meta for the given entry
- *
- * @param {DataCatalogEntry} dataCatalogEntry
- * @param {Object} [apiOptions]
- * @param {boolean} [apiOptions.silenceErrors] - Default true
- *
- * @return {CancellableJqPromise}
- */
-const reloadNavigatorMeta = function (dataCatalogEntry, apiOptions) {
-  if (dataCatalogEntry.canHaveNavigatorMetadata()) {
-    return dataCatalogEntry
-      .trackedPromise(
-        promise => {
-          dataCatalogEntry.navigatorMetaPromise = promise;
-        },
-        catalogUtils.fetchAndSave(
-          apiHelper.fetchNavigatorMetadata.bind(apiHelper),
-          val => {
-            dataCatalogEntry.navigatorMeta = val;
-          },
-          dataCatalogEntry,
-          apiOptions
-        )
-      )
-      .done(navigatorMeta => {
-        if (navigatorMeta && dataCatalogEntry.commentObservable) {
-          dataCatalogEntry.commentObservable(dataCatalogEntry.getResolvedComment());
-        }
-      });
-  }
-  dataCatalogEntry.navigatorMetaPromise = $.Deferred().reject();
-  return dataCatalogEntry.navigatorMetaPromise;
-};
-
-/**
- * Helper function to reload the analysis for the given entry
- *
- * @param {DataCatalogEntry} dataCatalogEntry
- * @param {Object} [apiOptions]
- * @param {boolean} [apiOptions.silenceErrors]
- * @param {boolean} [apiOptions.refreshAnalysis]
- *
- * @return {CancellableJqPromise}
- */
-const reloadAnalysis = function (dataCatalogEntry, apiOptions) {
-  return dataCatalogEntry.trackedPromise(
-    promise => {
-      dataCatalogEntry.analysisPromise = promise;
-    },
-    catalogUtils.fetchAndSave(
-      apiOptions && apiOptions.refreshAnalysis
-        ? apiHelper.refreshAnalysis.bind(apiHelper)
-        : apiHelper.fetchAnalysis.bind(apiHelper),
-      val => {
-        dataCatalogEntry.analysis = val;
-      },
-      dataCatalogEntry,
-      apiOptions
-    )
-  );
-};
-
-/**
- * Helper function to reload the partitions for the given entry
- *
- * @param {DataCatalogEntry} dataCatalogEntry
- * @param {Object} [apiOptions]
- * @param {boolean} [apiOptions.silenceErrors]
- *
- * @return {CancellableJqPromise}
- */
-const reloadPartitions = function (dataCatalogEntry, apiOptions) {
-  return dataCatalogEntry.trackedPromise(
-    promise => {
-      dataCatalogEntry.partitionsPromise = promise;
-    },
-    catalogUtils.fetchAndSave(
-      apiHelper.fetchPartitions.bind(apiHelper),
-      val => {
-        dataCatalogEntry.partitions = val;
-      },
-      dataCatalogEntry,
-      apiOptions
-    )
-  );
-};
-
-/**
- * Helper function to reload the sample for the given entry
- *
- * @param {DataCatalogEntry} dataCatalogEntry
- * @param {Object} [apiOptions]
- * @param {boolean} [apiOptions.silenceErrors]
- *
- * @return {CancellableJqPromise}
- */
-const reloadSample = function (dataCatalogEntry, apiOptions) {
-  return dataCatalogEntry.trackedPromise(
-    promise => {
-      dataCatalogEntry.samplePromise = promise;
-    },
-    catalogUtils.fetchAndSave(
-      apiHelper.fetchSample.bind(apiHelper),
-      val => {
-        dataCatalogEntry.sample = val;
-      },
-      dataCatalogEntry,
-      apiOptions
-    )
-  );
-};
-
-/**
- * Helper function to reload the nav opt metadata for the given entry
- *
- * @param {DataCatalogEntry} dataCatalogEntry
- * @param {Object} [apiOptions]
- * @param {boolean} [apiOptions.silenceErrors] - Default true
- *
- * @return {CancellableJqPromise}
- */
-const reloadOptimizerMeta = function (dataCatalogEntry, apiOptions) {
-  if (dataCatalogEntry.dataCatalog.canHaveOptimizerMeta()) {
-    const optimizer = getOptimizer(dataCatalogEntry.dataCatalog.connector);
-    return dataCatalogEntry.trackedPromise(
-      promise => {
-        dataCatalogEntry.optimizerMetaPromise = promise;
-      },
-      catalogUtils.fetchAndSave(
-        optimizer.fetchOptimizerMeta.bind(optimizer),
-        val => {
-          dataCatalogEntry.optimizerMeta = val;
-        },
-        dataCatalogEntry,
-        apiOptions
-      )
-    );
-  }
-  dataCatalogEntry.optimizerMetaPromise = $.Deferred.reject().promise();
-  return dataCatalogEntry.optimizerMetaPromise;
-};
-
-/**
- * Helper function to get details from the multi-table catalog for just this specific table
- *
- * @param {DataCatalogEntry} catalogEntry
- * @param {Object} [options]
- * @param {boolean} [options.silenceErrors] - Default false
- * @param {boolean} [options.cachedOnly] - Default false
- * @param {boolean} [options.refreshCache] - Default false
- * @param {boolean} [options.cancellable] - Default false
- * @param {string} functionName - The function to call, i.e. 'getTopAggs' etc.
- * @return {CancellableJqPromise}
- */
-const getFromMultiTableCatalog = function (catalogEntry, options, functionName) {
-  const deferred = $.Deferred();
-  if (!catalogEntry.isTableOrView()) {
-    return deferred.reject();
-  }
-  const cancellablePromises = [];
-  catalogEntry.dataCatalog
-    .getMultiTableEntry({
-      namespace: catalogEntry.namespace,
-      compute: catalogEntry.compute,
-      paths: [catalogEntry.path]
-    })
-    .done(multiTableEntry => {
-      cancellablePromises.push(
-        multiTableEntry[functionName](options).done(deferred.resolve).fail(deferred.reject)
-      );
-    })
-    .fail(deferred.reject);
-  return new CancellableJqPromise(deferred, undefined, cancellablePromises);
-};
-
-/**
- * @param {DataCatalog} options.dataCatalog
- * @param {string|string[]} options.path
- * @param {ContextNamespace} options.namespace - The context namespace
- * @param {ContextCompute} options.compute - The context compute
- * @param {Object} options.definition - Initial known metadata on creation (normally comes from the parent entry)
- * @param {boolean} [options.isTemporary] - Default false
- *
- * @constructor
- */
-export default class DataCatalogEntry {
-  constructor(options) {
-    const self = this;
-
-    if (!options.dataCatalog.connector) {
-      throw new Error('DataCatalogEntry created without connector');
-    }
-
-    self.namespace = options.namespace;
-    self.compute = options.compute;
-    self.dataCatalog = options.dataCatalog;
-
-    self.path = typeof options.path === 'string' ? options.path.split('.') : options.path || [];
-    self.name = self.path.length ? self.path[self.path.length - 1] : self.getConnector().id;
-    self.isTemporary = options.isTemporary;
-
-    self.definition = options.definition;
-
-    if (!self.definition) {
-      if (self.path.length === 0) {
-        self.definition = { type: 'source' };
-      } else if (self.path.length === 1) {
-        self.definition = { type: 'database' };
-      } else if (self.path.length === 2) {
-        self.definition = { type: 'table' };
-      }
-    }
-
-    self.reset();
-  }
-
-  /**
-   * Resets the entry to an empty state, it might still have some details cached
-   */
-  reset() {
-    const self = this;
-    self.saveTimeout = -1;
-    self.sourceMetaPromise = undefined;
-    self.sourceMeta = undefined;
-
-    self.navigatorMeta = undefined;
-    self.navigatorMetaPromise = undefined;
-
-    self.analysis = undefined;
-    self.analysisPromise = undefined;
-
-    self.partitions = undefined;
-    self.partitionsPromise = undefined;
-
-    self.sample = undefined;
-    self.samplePromise = undefined;
-
-    self.optimizerPopularity = undefined;
-    self.optimizerMeta = undefined;
-    self.optimizerMetaPromise = undefined;
-
-    self.navigatorMetaForChildrenPromise = undefined;
-    self.optimizerPopularityForChildrenPromise = undefined;
-
-    self.childrenPromise = undefined;
-
-    if (self.path.length) {
-      const parent = self.dataCatalog.getKnownEntry({
-        namespace: self.namespace,
-        compute: self.compute,
-        path: self.path.slice(0, self.path.length - 1)
-      });
-      if (parent) {
-        parent.navigatorMetaForChildrenPromise = undefined;
-        parent.optimizerPopularityForChildrenPromise = undefined;
-      }
-    }
-  }
-
-  /**
-   * Helper function that ensure that cancellable promises are not tracked anymore when cancelled
-   *
-   * @param {Function} promiseSetter - The promise attribute to set
-   * @param {CancellableJqPromise} cancellableJqPromise
-   */
-  trackedPromise(promiseSetter, cancellableJqPromise) {
-    promiseSetter(cancellableJqPromise);
-    return cancellableJqPromise.fail(() => {
-      if (cancellableJqPromise.cancelled) {
-        promiseSetter(undefined);
-      }
-    });
-  }
-
-  /**
-   * Resets the entry and clears the cache
-   *
-   * @param {Object} options
-   * @param {boolean} [options.cascade] - Default false, only used when the entry is for the source
-   * @param {boolean} [options.silenceErrors] - Default false
-   * @param {string} [options.targetChild] - Optional specific child to invalidate
-   * @return {CancellableJqPromise}
-   */
-  clearCache(options) {
-    const self = this;
-
-    if (!options) {
-      options = {};
-    }
-
-    if (self.definition && self.definition.optimizerLoaded) {
-      delete self.definition.optimizerLoaded;
-    }
-
-    self.reset();
-    const saveDeferred = options.cascade
-      ? self.dataCatalog.clearStorageCascade(self.namespace, self.compute, self.path)
-      : self.save();
-
-    saveDeferred.always(() => {
-      huePubSub.publish('data.catalog.entry.refreshed', {
-        entry: self,
-        cascade: !!options.cascade
-      });
-    });
-
-    return new CancellableJqPromise(saveDeferred, undefined, []);
-  }
-
-  /**
-   * Save the entry to cache
-   *
-   * @return {Promise}
-   */
-  save() {
-    const self = this;
-    window.clearTimeout(self.saveTimeout);
-    return self.dataCatalog.persistCatalogEntry(self);
-  }
-
-  /**
-   * Save the entry at a later point of time
-   */
-  saveLater() {
-    const self = this;
-    if (CACHEABLE_TTL.default > 0) {
-      window.clearTimeout(self.saveTimeout);
-      self.saveTimeout = window.setTimeout(() => {
-        self.save();
-      }, 1000);
-    }
-  }
-
-  /**
-   * Gets the parent entry, rejected if there's no parent.
-   *
-   * @return {Promise}
-   */
-  getParent() {
-    if (!this.path.length) {
-      return $.Deferred().reject().promise();
-    }
-
-    return this.dataCatalog.getEntry({
-      namespace: this.namespace,
-      compute: this.compute,
-      path: this.path.slice(0, this.path.length - 1)
-    });
-  }
-
-  /**
-   * Get the children of the catalog entry, columns for a table entry etc.
-   *
-   * @param {Object} [options]
-   * @param {boolean} [options.silenceErrors]
-   * @param {boolean} [options.cachedOnly]
-   * @param {boolean} [options.refreshCache]
-   * @param {boolean} [options.cancellable] - Default false
-   *
-   * @return {CancellableJqPromise}
-   */
-  getChildren(options) {
-    const self = this;
-    if (self.childrenPromise && DataCatalog.cacheEnabled() && (!options || !options.refreshCache)) {
-      return catalogUtils.applyCancellable(self.childrenPromise, options);
-    }
-    const deferred = $.Deferred();
-
-    if (
-      DataCatalog.cacheEnabled() &&
-      options &&
-      options.cachedOnly &&
-      !self.sourceMeta &&
-      !self.sourceMetaPromise
-    ) {
-      return deferred.reject(false).promise();
-    }
-
-    const sourceMetaPromise = self
-      .getSourceMeta(options)
-      .done(sourceMeta => {
-        if (!sourceMeta || sourceMeta.notFound) {
-          deferred.reject('No source meta found');
-          return;
-        }
-        const promises = [];
-        let index = 0;
-        const partitionKeys = {};
-        if (sourceMeta.partition_keys) {
-          sourceMeta.partition_keys.forEach(partitionKey => {
-            partitionKeys[partitionKey.name] = true;
-          });
-        }
-        const primaryKeys = {};
-        if (sourceMeta.primary_keys) {
-          sourceMeta.primary_keys.forEach(primaryKey => {
-            primaryKeys[primaryKey.name] = true;
-          });
-        }
-        const foreignKeys = {};
-        if (sourceMeta.foreign_keys) {
-          sourceMeta.foreign_keys.forEach(foreignKey => {
-            foreignKeys[foreignKey.name] = foreignKey;
-          });
-        }
-
-        const entities =
-          sourceMeta.databases ||
-          sourceMeta.tables_meta ||
-          sourceMeta.extended_columns ||
-          sourceMeta.fields ||
-          sourceMeta.columns;
-
-        if (entities) {
-          entities.forEach(entity => {
-            if (!sourceMeta.databases || (entity.name || entity) !== '_impala_builtins') {
-              promises.push(
-                self.dataCatalog
-                  .getEntry({
-                    namespace: self.namespace,
-                    compute: self.compute,
-                    path: self.path.concat(entity.name || entity)
-                  })
-                  .done(catalogEntry => {
-                    if (
-                      !catalogEntry.definition ||
-                      typeof catalogEntry.definition.index === 'undefined'
-                    ) {
-                      const definition = typeof entity === 'object' ? entity : {};
-                      if (typeof entity !== 'object') {
-                        if (self.path.length === 0) {
-                          definition.type = 'database';
-                        } else if (self.path.length === 1) {
-                          definition.type = 'table';
-                        } else if (self.path.length === 2) {
-                          definition.type = 'column';
-                        }
-                      }
-                      if (sourceMeta.partition_keys) {
-                        definition.partitionKey = !!partitionKeys[entity.name];
-                      }
-                      if (sourceMeta.primary_keys) {
-                        definition.primaryKey = !!primaryKeys[entity.name];
-                      }
-                      if (sourceMeta.foreign_keys) {
-                        definition.foreignKey = foreignKeys[entity.name];
-                      }
-                      definition.index = index++;
-                      catalogEntry.definition = definition;
-                      catalogEntry.saveLater();
-                    }
-                  })
-              );
-            }
-          });
-        }
-        // TODO: Move to connector attributes
-        if ((self.getDialect() === 'impala' || self.getDialect() === 'hive') && self.isComplex()) {
-          (sourceMeta.type === 'map' ? ['key', 'value'] : ['item']).forEach(path => {
-            if (sourceMeta[path]) {
-              promises.push(
-                self.dataCatalog
-                  .getEntry({
-                    namespace: self.namespace,
-                    compute: self.compute,
-                    path: self.path.concat(path)
-                  })
-                  .done(catalogEntry => {
-                    if (
-                      !catalogEntry.definition ||
-                      typeof catalogEntry.definition.index === 'undefined'
-                    ) {
-                      const definition = sourceMeta[path];
-                      definition.index = index++;
-                      definition.isMapValue = path === 'value';
-                      catalogEntry.definition = definition;
-                      catalogEntry.saveLater();
-                    }
-                  })
-              );
-            }
-          });
-        }
-        $.when.apply($, promises).done(function () {
-          deferred.resolve(Array.prototype.slice.call(arguments));
-        });
-      })
-      .fail(deferred.reject);
-
-    return catalogUtils.applyCancellable(
-      self.trackedPromise(promise => {
-        self.childrenPromise = promise;
-      }, new CancellableJqPromise(deferred, undefined, [sourceMetaPromise])),
-      options
-    );
-  }
-
-  /**
-   * Loads navigator metadata for children, only applicable to databases and tables
-   *
-   * @param {Object} [options]
-   * @param {boolean} [options.refreshCache]
-   * @param {boolean} [options.silenceErrors] - Default true
-   * @param {boolean} [options.cancellable] - Default false
-   *
-   * @return {CancellableJqPromise}
-   */
-  loadNavigatorMetaForChildren(options) {
-    const self = this;
-
-    options = catalogUtils.setSilencedErrors(options);
-
-    if (!self.canHaveNavigatorMetadata() || self.isField()) {
-      return $.Deferred().reject().promise();
-    }
-
-    if (
-      self.navigatorMetaForChildrenPromise &&
-      DataCatalog.cacheEnabled() &&
-      (!options || !options.refreshCache)
-    ) {
-      return catalogUtils.applyCancellable(self.navigatorMetaForChildrenPromise, options);
-    }
-
-    const deferred = $.Deferred();
-
-    const cancellablePromises = [];
-
-    cancellablePromises.push(
-      self
-        .getChildren(options)
-        .done(children => {
-          const someHaveNavMeta = children.some(childEntry => {
-            return childEntry.navigatorMeta;
-          });
-          if (
-            someHaveNavMeta &&
-            DataCatalog.cacheEnabled() &&
-            (!options || !options.refreshCache)
-          ) {
-            deferred.resolve(children);
-            return;
-          }
-
-          let query;
-          // TODO: Add sourceType to nav search query
-          if (self.path.length) {
-            query = 'parentPath:"/' + self.path.join('/') + '" AND type:(table view field)';
-          } else {
-            query = 'type:database';
-          }
-
-          const rejectUnknown = () => {
-            children.forEach(childEntry => {
-              if (!childEntry.navigatorMeta) {
-                childEntry.navigatorMeta = {};
-                childEntry.navigatorMetaPromise = $.Deferred().reject().promise();
-              }
-            });
-          };
-
-          cancellablePromises.push(
-            apiHelper
-              .searchEntities({
-                query: query,
-                rawQuery: true,
-                limit: children.length,
-                silenceErrors: options && options.silenceErrors
-              })
-              .done(result => {
-                if (result && result.entities) {
-                  const childEntryIndex = {};
-                  children.forEach(childEntry => {
-                    childEntryIndex[childEntry.name.toLowerCase()] = childEntry;
-                  });
-
-                  result.entities.forEach(entity => {
-                    const matchingChildEntry =
-                      childEntryIndex[(entity.original_name || entity.originalName).toLowerCase()];
-                    if (matchingChildEntry) {
-                      matchingChildEntry.navigatorMeta = entity;
-                      entity.hueTimestamp = Date.now();
-                      matchingChildEntry.navigatorMetaPromise = $.Deferred()
-                        .resolve(matchingChildEntry.navigatorMeta)
-                        .promise();
-                      if (entity && matchingChildEntry.commentObservable) {
-                        matchingChildEntry.commentObservable(
-                          matchingChildEntry.getResolvedComment()
-                        );
-                      }
-                      matchingChildEntry.saveLater();
-                    }
-                  });
-                }
-                rejectUnknown();
-                deferred.resolve(children);
-              })
-              .fail(() => {
-                rejectUnknown();
-                deferred.reject();
-              })
-          );
-        })
-        .fail(deferred.reject)
-    );
-
-    return catalogUtils.applyCancellable(
-      self.trackedPromise(promise => {
-        self.navigatorMetaForChildrenPromise = promise;
-      }, new CancellableJqPromise(deferred, null, cancellablePromises)),
-      options
-    );
-  }
-
-  /**
-   * Helper function used when loading navopt metdata for children
-   *
-   * @param {Object} response
-   * @param {Object} [options]
-   * @param {boolean} [options.silenceErrors] - Default false
-   *
-   * @return {CancellableJqPromise}
-   */
-  applyOptimizerResponseToChildren(response, options) {
-    const self = this;
-    const deferred = $.Deferred();
-    if (!self.definition) {
-      self.definition = {};
-    }
-    self.definition.optimizerLoaded = true;
-    self.saveLater();
-
-    const childPromise = self
-      .getChildren(options)
-      .done(childEntries => {
-        const entriesByName = {};
-        childEntries.forEach(childEntry => {
-          entriesByName[childEntry.name.toLowerCase()] = childEntry;
-        });
-        const updatedIndex = {};
-
-        if (self.isDatabase() && response.top_tables) {
-          response.top_tables.forEach(topTable => {
-            const matchingChild = entriesByName[topTable.name.toLowerCase()];
-            if (matchingChild) {
-              matchingChild.optimizerPopularity = topTable;
-              matchingChild.saveLater();
-              updatedIndex[matchingChild.getQualifiedPath()] = matchingChild;
-            }
-          });
-        } else if (self.isTableOrView() && response.values) {
-          const addOptimizerPopularity = function (columns, type) {
-            if (columns) {
-              columns.forEach(column => {
-                const matchingChild = entriesByName[column.columnName.toLowerCase()];
-                if (matchingChild) {
-                  if (!matchingChild.optimizerPopularity) {
-                    matchingChild.optimizerPopularity = {};
-                  }
-                  matchingChild.optimizerPopularity[type] = column;
-                  matchingChild.saveLater();
-                  updatedIndex[matchingChild.getQualifiedPath()] = matchingChild;
-                }
-              });
-            }
-          };
-
-          addOptimizerPopularity(response.values.filterColumns, 'filterColumn');
-          addOptimizerPopularity(response.values.groupbyColumns, 'groupByColumn');
-          addOptimizerPopularity(response.values.joinColumns, 'joinColumn');
-          addOptimizerPopularity(response.values.orderbyColumns, 'orderByColumn');
-          addOptimizerPopularity(response.values.selectColumns, 'selectColumn');
-        }
-        const popularEntries = [];
-        Object.keys(updatedIndex).forEach(path => {
-          popularEntries.push(updatedIndex[path]);
-        });
-        deferred.resolve(popularEntries);
-      })
-      .fail(deferred.reject);
-
-    return new CancellableJqPromise(deferred, undefined, [childPromise]);
-  }
-
-  /**
-   * Loads nav opt popularity for the children of this entry.
-   *
-   * @param {Object} [options]
-   * @param {boolean} [options.refreshCache]
-   * @param {boolean} [options.silenceErrors] - Default true
-   * @param {boolean} [options.cancellable] - Default false
-   *
-   * @return {CancellableJqPromise}
-   */
-  loadOptimizerPopularityForChildren(options) {
-    const self = this;
-
-    options = catalogUtils.setSilencedErrors(options);
-
-    if (!self.dataCatalog.canHaveOptimizerMeta()) {
-      return $.Deferred().reject().promise();
-    }
-    if (
-      self.optimizerPopularityForChildrenPromise &&
-      DataCatalog.cacheEnabled() &&
-      (!options || !options.refreshCache)
-    ) {
-      return catalogUtils.applyCancellable(self.optimizerPopularityForChildrenPromise, options);
-    }
-    const deferred = $.Deferred();
-    const cancellablePromises = [];
-    if (
-      self.definition &&
-      self.definition.optimizerLoaded &&
-      DataCatalog.cacheEnabled() &&
-      (!options || !options.refreshCache)
-    ) {
-      cancellablePromises.push(
-        self
-          .getChildren(options)
-          .done(childEntries => {
-            deferred.resolve(
-              childEntries.filter(entry => {
-                return entry.optimizerPopularity;
-              })
-            );
-          })
-          .fail(deferred.reject)
-      );
-    } else if (self.isDatabase() || self.isTableOrView()) {
-      cancellablePromises.push(
-        getOptimizer(self.dataCatalog.connector)
-          .fetchPopularity({
-            silenceErrors: options && options.silenceErrors,
-            refreshCache: options && options.refreshCache,
-            paths: [self.path]
-          })
-          .done(data => {
-            cancellablePromises.push(
-              self
-                .applyOptimizerResponseToChildren(data, options)
-                .done(deferred.resolve)
-                .fail(deferred.reject)
-            );
-          })
-          .fail(deferred.reject)
-      );
-    } else {
-      deferred.resolve([]);
-    }
-
-    return catalogUtils.applyCancellable(
-      self.trackedPromise(promise => {
-        self.optimizerPopularityForChildrenPromise = promise;
-      }, new CancellableJqPromise(deferred, undefined, cancellablePromises)),
-      options
-    );
-  }
-
-  /**
-   * Returns true if the catalog entry can have navigator metadata
-   *
-   * @return {boolean}
-   */
-  canHaveNavigatorMetadata() {
-    const self = this;
-    // TODO: Move to connector attributes
-    return (
-      window.HAS_CATALOG &&
-      (self.getDialect() === 'hive' || self.getDialect() === 'impala') &&
-      (self.isDatabase() || self.isTableOrView() || self.isColumn())
-    );
-  }
-
-  /**
-   * Returns the currently known comment without loading any additional metadata
-   *
-   * @return {string}
-   */
-  getResolvedComment() {
-    const self = this;
-    // TODO: Move to connector attributes
-    if (
-      self.navigatorMeta &&
-      (self.getDialect() === 'hive' || self.getDialect() === 'impala') &&
-      (self.navigatorMeta.description || self.navigatorMeta.originalDescription)
-    ) {
-      return self.navigatorMeta.description || self.navigatorMeta.originalDescription;
-    }
-    if (self.definition && self.definition.comment) {
-      return self.definition.comment;
-    }
-    return (self.sourceMeta && self.sourceMeta.comment) || '';
-  }
-
-  /**
-   * This can be used to get an observable for the comment which will be updated once a comment has been
-   * resolved.
-   *
-   * @return {ko.observable}
-   */
-  getCommentObservable() {
-    const self = this;
-    if (!self.commentObservable) {
-      self.commentObservable = ko.observable(self.getResolvedComment());
-    }
-    return self.commentObservable;
-  }
-
-  /**
-   * Checks whether the comment is known and has been loaded from the proper source
-   *
-   * @return {boolean}
-   */
-  hasResolvedComment() {
-    const self = this;
-    if (self.canHaveNavigatorMetadata()) {
-      return typeof self.navigatorMeta !== 'undefined';
-    }
-    return typeof self.sourceMeta !== 'undefined';
-  }
-
-  /**
-   * Gets the comment for this entry, fetching it if necessary from the proper source.
-   *
-   * @param {Object} [options]
-   * @param {boolean} [options.silenceErrors]
-   * @param {boolean} [options.cachedOnly]
-   * @param {boolean} [options.refreshCache]
-   * @param {boolean} [options.cancellable] - Default false
-   *
-   * @return {CancellableJqPromise}
-   */
-  getComment(options) {
-    const self = this;
-    const deferred = $.Deferred();
-    const cancellablePromises = [];
-
-    const resolveWithSourceMeta = function () {
-      if (self.sourceMeta) {
-        deferred.resolve(self.sourceMeta.comment || '');
-      } else if (self.definition && typeof self.definition.comment !== 'undefined') {
-        deferred.resolve(self.definition.comment);
-      } else {
-        cancellablePromises.push(
-          self
-            .getSourceMeta(options)
-            .done(sourceMeta => {
-              deferred.resolve((sourceMeta && sourceMeta.comment) || '');
-            })
-            .fail(deferred.reject)
-        );
-      }
-    };
-
-    if (self.canHaveNavigatorMetadata()) {
-      if (self.navigatorMetaPromise) {
-        self.navigatorMetaPromise
-          .done(() => {
-            deferred.resolve(
-              self.navigatorMeta.description || self.navigatorMeta.originalDescription || ''
-            );
-          })
-          .fail(resolveWithSourceMeta);
-      } else if (self.navigatorMeta) {
-        deferred.resolve(
-          self.navigatorMeta.description || self.navigatorMeta.originalDescription || ''
-        );
-      } else {
-        cancellablePromises.push(
-          self
-            .getNavigatorMeta(options)
-            .done(navigatorMeta => {
-              if (navigatorMeta) {
-                deferred.resolve(
-                  navigatorMeta.description || navigatorMeta.originalDescription || ''
-                );
-              } else {
-                resolveWithSourceMeta();
-              }
-            })
-            .fail(resolveWithSourceMeta)
-        );
-      }
-    } else {
-      resolveWithSourceMeta();
-    }
-
-    return catalogUtils.applyCancellable(
-      new CancellableJqPromise(deferred, undefined, cancellablePromises),
-      options
-    );
-  }
-
-  /**
-   * Updates custom navigator metadata for the catalog entry
-   *
-   * @param {Object} [modifiedCustomMetadata] - The custom metadata to update, only supply what has been changed
-   * @param {string[]} [deletedCustomMetadataKeys] - The custom metadata to delete identifier by the keys
-   * @param {Object} [apiOptions]
-   * @param {boolean} [apiOptions.silenceErrors]
-   *
-   * @return {Promise}
-   */
-  updateNavigatorCustomMetadata(modifiedCustomMetadata, deletedCustomMetadataKeys, apiOptions) {
-    const self = this;
-    const deferred = $.Deferred();
-
-    if (self.canHaveNavigatorMetadata()) {
-      if (
-        self.navigatorMeta === {} ||
-        (self.navigatorMeta && typeof self.navigatorMeta.identity === 'undefined')
-      ) {
-        if (!apiOptions) {
-          apiOptions = {};
-        }
-        apiOptions.refreshCache = true;
-      }
-      self
-        .getNavigatorMeta(apiOptions)
-        .done(navigatorMeta => {
-          if (navigatorMeta) {
-            apiHelper
-              .updateNavigatorProperties({
-                identity: navigatorMeta.identity,
-                modifiedCustomMetadata: modifiedCustomMetadata,
-                deletedCustomMetadataKeys: deletedCustomMetadataKeys
-              })
-              .done(entity => {
-                if (entity) {
-                  self.navigatorMeta = entity;
-                  self.navigatorMetaPromise = $.Deferred().resolve(self.navigatorMeta).promise();
-                  self.saveLater();
-                  deferred.resolve(self.navigatorMeta);
-                } else {
-                  deferred.reject();
-                }
-              })
-              .fail(deferred.reject);
-          }
-        })
-        .fail(deferred.reject);
-    } else {
-      deferred.reject();
-    }
-
-    return deferred.promise();
-  }
-
-  /**
-   * Sets the comment in the proper source
-   *
-   * @param {string} comment
-   * @param {Object} [apiOptions]
-   * @param {boolean} [apiOptions.silenceErrors]
-   * @param {boolean} [apiOptions.refreshCache]
-   *
-   * @return {Promise}
-   */
-  setComment(comment, apiOptions) {
-    const self = this;
-    const deferred = $.Deferred();
-
-    if (self.canHaveNavigatorMetadata()) {
-      if (
-        self.navigatorMeta === {} ||
-        (self.navigatorMeta && typeof self.navigatorMeta.identity === 'undefined')
-      ) {
-        if (!apiOptions) {
-          apiOptions = {};
-        }
-        apiOptions.refreshCache = true;
-      }
-      self
-        .getNavigatorMeta(apiOptions)
-        .done(navigatorMeta => {
-          if (navigatorMeta) {
-            apiHelper
-              .updateNavigatorProperties({
-                identity: navigatorMeta.identity,
-                properties: {
-                  description: comment
-                }
-              })
-              .done(entity => {
-                if (entity) {
-                  self.navigatorMeta = entity;
-                  self.navigatorMetaPromise = $.Deferred().resolve(self.navigatorMeta).promise();
-                  self.saveLater();
-                }
-                self.getComment(apiOptions).done(comment => {
-                  if (self.commentObservable && self.commentObservable() !== comment) {
-                    self.commentObservable(comment);
-                  }
-                  deferred.resolve(comment);
-                });
-              })
-              .fail(deferred.reject);
-          }
-        })
-        .fail(deferred.reject);
-    } else {
-      apiHelper
-        .updateSourceMetadata({
-          sourceType: self.getConnector().id,
-          path: self.path,
-          properties: {
-            comment: comment
-          }
-        })
-        .done(() => {
-          reloadSourceMeta(self, {
-            silenceErrors: apiOptions && apiOptions.silenceErrors,
-            refreshCache: true
-          }).done(() => {
-            self.getComment(apiOptions).done(deferred.resolve);
-          });
-        })
-        .fail(deferred.reject);
-    }
-
-    return deferred.promise();
-  }
-
-  /**
-   * Adds a list of tags and updates the navigator metadata of the entry
-   *
-   * @param {string[]} tags
-   *
-   * @return {Promise}
-   */
-  addNavigatorTags(tags) {
-    const self = this;
-    const deferred = $.Deferred();
-    if (self.canHaveNavigatorMetadata()) {
-      self
-        .getNavigatorMeta()
-        .done(navMeta => {
-          if (navMeta && typeof navMeta.identity !== 'undefined') {
-            apiHelper
-              .addNavTags(navMeta.identity, tags)
-              .done(entity => {
-                if (entity) {
-                  self.navigatorMeta = entity;
-                  self.navigatorMetaPromise = $.Deferred().resolve(self.navigatorMeta).promise();
-                  self.saveLater();
-                } else {
-                  deferred.reject();
-                }
-                deferred.resolve(self.navigatorMeta);
-              })
-              .fail(deferred.reject);
-          } else {
-            deferred.reject();
-          }
-        })
-        .fail(deferred.reject);
-    } else {
-      deferred.reject();
-    }
-    return deferred.promise();
-  }
-
-  /**
-   * Removes a list of tags and updates the navigator metadata of the entry
-   *
-   * @param {string[]} tags
-   *
-   * @return {Promise}
-   */
-  deleteNavigatorTags(tags) {
-    const self = this;
-    const deferred = $.Deferred();
-    if (self.canHaveNavigatorMetadata()) {
-      self
-        .getNavigatorMeta()
-        .done(navMeta => {
-          if (navMeta && typeof navMeta.identity !== 'undefined') {
-            apiHelper
-              .deleteNavTags(navMeta.identity, tags)
-              .done(entity => {
-                if (entity) {
-                  self.navigatorMeta = entity;
-                  self.navigatorMetaPromise = $.Deferred().resolve(self.navigatorMeta).promise();
-                  self.saveLater();
-                } else {
-                  deferred.reject();
-                }
-                deferred.resolve(self.navigatorMeta);
-              })
-              .fail(deferred.reject);
-          } else {
-            deferred.reject();
-          }
-        })
-        .fail(deferred.reject);
-    } else {
-      deferred.reject();
-    }
-    return deferred.promise();
-  }
-
-  /**
-   * Checks if the entry can have children or not without fetching additional metadata.
-   *
-   * @return {boolean}
-   */
-  hasPossibleChildren() {
-    const self = this;
-    return (
-      self.path.length < 3 ||
-      (!self.definition && !self.sourceMeta) ||
-      (self.sourceMeta && /^(?:struct|array|map)/i.test(self.sourceMeta.type)) ||
-      (self.definition && /^(?:struct|array|map)/i.test(self.definition.type))
-    );
-  }
-
-  /**
-   * Returns the index representing the order in which the backend returned this entry.
-   *
-   * @return {number}
-   */
-  getIndex() {
-    const self = this;
-    return self.definition && self.definition.index ? self.definition.index : 0;
-  }
-
-  /**
-   * Returns the dialect of this entry.
-   *
-   * @return {string} - 'impala', 'hive', 'solr', etc.
-   */
-  getDialect() {
-    return this.getConnector().dialect || this.getConnector().id; // .id for editor v1
-  }
-
-  /**
-   * Returns the connector for this entry
-   *
-   * @return {Connector}
-   */
-  getConnector() {
-    return this.dataCatalog.connector;
-  }
-
-  /**
-   * Returns true if the entry represents a data source.
-   *
-   * @return {boolean}
-   */
-  isSource() {
-    const self = this;
-    return self.path.length === 0;
-  }
-
-  /**
-   * Returns true if the entry is a database.
-   *
-   * @return {boolean}
-   */
-  isDatabase() {
-    const self = this;
-    return self.path.length === 1;
-  }
-
-  /**
-   * Returns true if the entry is a table or a view.
-   *
-   * @return {boolean}
-   */
-  isTableOrView() {
-    const self = this;
-    return self.path.length === 2;
-  }
-
-  /**
-   * Returns the default title used for the entry, the qualified path with type for fields. Optionally include
-   * the comment after, if already resolved.
-   *
-   * @param {boolean} [includeComment] - Default false
-   * @return {string}
-   */
-  getTitle(includeComment) {
-    const self = this;
-    let title = self.getQualifiedPath();
-    if (self.isField()) {
-      const type = self.getType();
-      if (type) {
-        title += ' (' + type + ')';
-      }
-    } else if (
-      self.definition &&
-      self.definition.type &&
-      self.definition.type.toLowerCase() === 'materialized_view'
-    ) {
-      title += ' (' + I18n('Materialized') + ')';
-    }
-    if (includeComment && self.hasResolvedComment() && self.getResolvedComment()) {
-      title += ' - ' + self.getResolvedComment();
-    }
-    return title;
-  }
-
-  /**
-   * Returns the fully qualified path for this entry.
-   *
-   * @return {string}
-   */
-  getQualifiedPath() {
-    const self = this;
-    return self.path.join('.');
-  }
-
-  /**
-   * Returns the display name for the entry, name or qualified path plus type for fields
-   *
-   * @param {boolean} qualified - Whether to use the qualified path or not, default false
-   * @return {string}
-   */
-  getDisplayName(qualified) {
-    const self = this;
-    let displayName = qualified ? self.getQualifiedPath() : self.name;
-    if (self.isField()) {
-      const type = self.getType();
-      if (type) {
-        displayName += ' (' + type + ')';
-      }
-    }
-    return displayName;
-  }
-
-  /**
-   * Returns true for columns that are a primary key. Note that the definition has to come from a parent entry, i.e.
-   * getChildren().
-   *
-   * @return {boolean}
-   */
-  isPrimaryKey() {
-    const self = this;
-    return self.isColumn() && self.definition && !!self.definition.primaryKey;
-  }
-
-  /**
-   * Returns true if the entry is a partition key. Note that the definition has to come from a parent entry, i.e.
-   * getChildren().
-   *
-   * @return {boolean}
-   */
-  isPartitionKey() {
-    const self = this;
-    return self.definition && !!self.definition.partitionKey;
-  }
-
-  /**
-   * Returns true if the entry is a foreign key. Note that the definition has to come from a parent entry, i.e.
-   * getChildren().
-   *
-   * @return {boolean}
-   */
-  isForeignKey() {
-    const self = this;
-    return self.definition && !!self.definition.foreignKey;
-  }
-
-  /**
-   * Returns true if the entry is either a partition or primary key. Note that the definition has to come from a parent entry, i.e.
-   * getChildren().
-   *
-   * @return {boolean}
-   */
-  isKey() {
-    return this.isPartitionKey() || this.isPrimaryKey() || this.isForeignKey();
-  }
-
-  /**
-   * Returns true if the entry is a table. It will be accurate once the source meta has been loaded.
-   *
-   * @return {boolean}
-   */
-  isTable() {
-    const self = this;
-    if (self.path.length === 2) {
-      if (
-        self.analysis &&
-        self.analysis.details &&
-        self.analysis.details.properties &&
-        self.analysis.details.properties.table_type === 'VIRTUAL_VIEW'
-      ) {
-        return false;
-      }
-      if (self.sourceMeta) {
-        return !self.sourceMeta.is_view;
-      }
-      if (self.definition && self.definition.type) {
-        return self.definition.type.toLowerCase() === 'table';
-      }
-      return true;
-    }
-    return false;
-  }
-
-  /**
-   * Returns true if the entry is a table. It will be accurate once the source meta has been loaded.
-   *
-   * @return {boolean}
-   */
-  isView() {
-    const self = this;
-    return (
-      self.path.length === 2 &&
-      ((self.sourceMeta && self.sourceMeta.is_view) ||
-        (self.definition &&
-          self.definition.type &&
-          (self.definition.type.toLowerCase() === 'view' ||
-            self.definition.type.toLowerCase() === 'materialized_view')) ||
-        (self.analysis &&
-          self.analysis.details &&
-          self.analysis.details.properties &&
-          self.analysis.details.properties.table_type === 'VIRTUAL_VIEW'))
-    );
-  }
-
-  /**
-   * Returns true if the entry is a ML Model. It will be accurate once the source meta has been loaded.
-   *
-   * @return {boolean}
-   */
-  isModel() {
-    const self = this;
-    return (
-      self.path.length === 2 &&
-      self.definition &&
-      self.definition.type &&
-      self.definition.type.toLowerCase() === 'model'
-    );
-  }
-
-  /**
-   * Returns true if the entry is a column.
-   *
-   * @return {boolean}
-   */
-  isColumn() {
-    const self = this;
-    return self.path.length === 3;
-  }
-
-  /**
-   * Returns true if the entry is a column. It will be accurate once the source meta has been loaded or if loaded from
-   * a parent entry via getChildren().
-   *
-   * @return {boolean}
-   */
-  isComplex() {
-    const 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)))
-    );
-  }
-
-  /**
-   * Returns true if the entry is a field, i.e. column or child of a complex type.
-   *
-   * @return {boolean}
-   */
-  isField() {
-    const self = this;
-    return self.path.length > 2;
-  }
-
-  /**
-   * Returns true if the entry is an array. It will be accurate once the source meta has been loaded or if loaded from
-   * a parent entry via getChildren().
-   *
-   * @return {boolean}
-   */
-  isArray() {
-    const self = this;
-    return (
-      (self.sourceMeta && /^array/i.test(self.sourceMeta.type)) ||
-      (self.definition && /^array/i.test(self.definition.type))
-    );
-  }
-
-  /**
-   * Returns true if the entry is a map. It will be accurate once the source meta has been loaded or if loaded from
-   * a parent entry via getChildren().
-   *
-   * @return {boolean}
-   */
-  isMap() {
-    const self = this;
-    return (
-      (self.sourceMeta && /^map/i.test(self.sourceMeta.type)) ||
-      (self.definition && /^map/i.test(self.definition.type))
-    );
-  }
-
-  /**
-   * Returns true if the entry is a map value. It will be accurate once the source meta has been loaded or if loaded
-   * from a parent entry via getChildren().
-   *
-   * @return {boolean}
-   */
-  isMapValue() {
-    const self = this;
-    return self.definition && self.definition.isMapValue;
-  }
-
-  /**
-   * Returns the type of the entry. It will be accurate once the source meta has been loaded or if loaded from
-   * a parent entry via getChildren().
-   *
-   * The returned string is always lower case and for complex entries the type definition is stripped to
-   * either 'array', 'map' or 'struct'.
-   *
-   * @return {string}
-   */
-  getType() {
-    const self = this;
-    let type = self.getRawType();
-    if (type.indexOf('<') !== -1) {
-      type = type.substring(0, type.indexOf('<'));
-    }
-    return type.toLowerCase();
-  }
-
-  /**
-   * Returns the raw type of the entry. It will be accurate once the source meta has been loaded or if loaded from
-   * a parent entry via getChildren().
-   *
-   * For complex entries the type definition is the full version.
-   *
-   * @return {string}
-   */
-  getRawType() {
-    const self = this;
-    return (self.sourceMeta && self.sourceMeta.type) || self.definition.type || '';
-  }
-
-  /**
-   * Gets the source metadata for the entry. It will fetch it if not cached or if the refresh option is set.
-   *
-   * @param {Object} [options]
-   * @param {boolean} [options.silenceErrors]
-   * @param {boolean} [options.cachedOnly]
-   * @param {boolean} [options.refreshCache]
-   * @param {boolean} [options.cancellable] - Default false
-   *
-   * @return {CancellableJqPromise<SourceMeta>}
-   */
-  getSourceMeta(options) {
-    const self = this;
-    if (options && options.cachedOnly) {
-      return (
-        catalogUtils.applyCancellable(self.sourceMetaPromise, options) ||
-        $.Deferred().reject(false).promise()
-      );
-    }
-    if (options && options.refreshCache) {
-      return catalogUtils.applyCancellable(reloadSourceMeta(self, options));
-    }
-    return catalogUtils.applyCancellable(
-      self.sourceMetaPromise || reloadSourceMeta(self, options),
-      options
-    );
-  }
-
-  /**
-   * Gets the analysis for the entry. It will fetch it if not cached or if the refresh option is set.
-   *
-   * @param {Object} [options]
-   * @param {boolean} [options.silenceErrors]
-   * @param {boolean} [options.cachedOnly]
-   * @param {boolean} [options.refreshCache] - Clears the browser cache
-   * @param {boolean} [options.refreshAnalysis] - Performs a hard refresh on the source level
-   * @param {boolean} [options.cancellable] - Default false
-   *
-   * @return {CancellableJqPromise}
-   */
-  getAnalysis(options) {
-    const self = this;
-    if (options && options.cachedOnly) {
-      return (
-        catalogUtils.applyCancellable(self.analysisPromise, options) ||
-        $.Deferred().reject(false).promise()
-      );
-    }
-    if (options && (options.refreshCache || options.refreshAnalysis)) {
-      return catalogUtils.applyCancellable(reloadAnalysis(self, options), options);
-    }
-    return catalogUtils.applyCancellable(
-      self.analysisPromise || reloadAnalysis(self, options),
-      options
-    );
-  }
-
-  /**
-   * Gets the partitions for the entry. It will fetch it if not cached or if the refresh option is set.
-   *
-   * @param {Object} [options]
-   * @param {boolean} [options.silenceErrors]
-   * @param {boolean} [options.cachedOnly]
-   * @param {boolean} [options.refreshCache] - Clears the browser cache
-   * @param {boolean} [options.cancellable] - Default false
-   *
-   * @return {CancellableJqPromise}
-   */
-  getPartitions(options) {
-    const self = this;
-    if (!self.isTableOrView()) {
-      return $.Deferred().reject(false).promise();
-    }
-    if (options && options.cachedOnly) {
-      return (
-        catalogUtils.applyCancellable(self.partitionsPromise, options) ||
-        $.Deferred().reject(false).promise()
-      );
-    }
-    if (options && options.refreshCache) {
-      return catalogUtils.applyCancellable(reloadPartitions(self, options), options);
-    }
-    return catalogUtils.applyCancellable(
-      self.partitionsPromise || reloadPartitions(self, options),
-      options
-    );
-  }
-
-  /**
-   * Gets the Navigator metadata for the entry. It will fetch it if not cached or if the refresh option is set.
-   *
-   * @param {Object} [options]
-   * @param {boolean} [options.silenceErrors] - Default true
-   * @param {boolean} [options.cachedOnly]
-   * @param {boolean} [options.refreshCache]
-   * @param {boolean} [options.cancellable] - Default false
-   *
-   * @return {CancellableJqPromise}
-   */
-  getNavigatorMeta(options) {
-    const self = this;
-
-    options = catalogUtils.setSilencedErrors(options);
-
-    if (!self.canHaveNavigatorMetadata()) {
-      return $.Deferred().reject().promise();
-    }
-    if (options && options.cachedOnly) {
-      return (
-        catalogUtils.applyCancellable(self.navigatorMetaPromise, options) ||
-        $.Deferred().reject(false).promise()
-      );
-    }
-    if (options && options.refreshCache) {
-      return catalogUtils.applyCancellable(reloadNavigatorMeta(self, options), options);
-    }
-    return catalogUtils.applyCancellable(
-      self.navigatorMetaPromise || reloadNavigatorMeta(self, options),
-      options
-    );
-  }
-
-  /**
-   * Gets the Nav Opt metadata for the entry. It will fetch it if not cached or if the refresh option is set.
-   *
-   * @param {Object} [options]
-   * @param {boolean} [options.silenceErrors] - Default true
-   * @param {boolean} [options.cachedOnly] - Default false
-   * @param {boolean} [options.refreshCache] - Default false
-   * @param {boolean} [options.cancellable] - Default false
-   *
-   * @return {CancellableJqPromise}
-   */
-  getOptimizerMeta(options) {
-    const self = this;
-
-    options = catalogUtils.setSilencedErrors(options);
-
-    if (!self.dataCatalog.canHaveOptimizerMeta() || !self.isTableOrView()) {
-      return $.Deferred().reject().promise();
-    }
-    if (options && options.cachedOnly) {
-      return (
-        catalogUtils.applyCancellable(self.optimizerMetaPromise, options) ||
-        $.Deferred().reject(false).promise()
-      );
-    }
-    if (options && options.refreshCache) {
-      return catalogUtils.applyCancellable(reloadOptimizerMeta(self, options), options);
-    }
-    return catalogUtils.applyCancellable(
-      self.optimizerMetaPromise || reloadOptimizerMeta(self, options),
-      options
-    );
-  }
-
-  /**
-   * Gets the sample for the entry, if unknown it will first check if any parent table already has the sample. It
-   * will fetch it if not cached or if the refresh option is set.
-   *
-   * @param {Object} [options]
-   * @param {boolean} [options.silenceErrors] - Default false
-   * @param {boolean} [options.cachedOnly] - Default false
-   * @param {boolean} [options.refreshCache] - Default false
-   * @param {boolean} [options.cancellable] - Default false
-   * @oaram {string} [options.operation]
-   *
-   * @return {CancellableJqPromise}
-   */
-  getSample(options) {
-    const self = this;
-
-    // This prevents caching of any non-standard sample queries, i.e. DISTINCT etc.
-    if (options && options.operation && options.operation !== 'default') {
-      return catalogUtils.applyCancellable(
-        apiHelper.fetchSample({
-          sourceType: self.getConnector().id,
-          compute: self.compute,
-          path: self.path,
-          silenceErrors: options && options.silenceErrors,
-          operation: options.operation
-        }),
-        options
-      );
-    }
-
-    // Check if parent has a sample that we can reuse
-    if (!self.samplePromise && self.isColumn()) {
-      const deferred = $.Deferred();
-      const cancellablePromises = [];
-
-      const revertToSpecific = function () {
-        if (options && options.cachedOnly) {
-          deferred.reject();
-        } else {
-          cancellablePromises.push(
-            catalogUtils
-              .applyCancellable(reloadSample(self, options), options)
-              .done(deferred.resolve)
-              .fail(deferred.reject)
-          );
-        }
-      };
-
-      self.dataCatalog
-        .getEntry({
-          namespace: self.namespace,
-          compute: self.compute,
-          path: self.path.slice(0, 2),
-          definition: { type: 'table' }
-        })
-        .done(tableEntry => {
-          if (tableEntry && tableEntry.samplePromise) {
-            cancellablePromises.push(
-              catalogUtils.applyCancellable(tableEntry.samplePromise, options)
-            );
-
-            tableEntry.samplePromise
-              .done(parentSample => {
-                const colSample = {
-                  hueTimestamp: parentSample.hueTimestamp,
-                  has_more: parentSample.has_more,
-                  type: parentSample.type,
-                  data: [],
-                  meta: []
-                };
-                if (parentSample.meta) {
-                  for (let i = 0; i < parentSample.meta.length; i++) {
-                    if (parentSample.meta[i].name.toLowerCase() === self.name.toLowerCase()) {
-                      colSample.meta[0] = parentSample.meta[i];
-                      parentSample.data.forEach(parentRow => {
-                        colSample.data.push([parentRow[i]]);
-                      });
-                      break;
-                    }
-                  }
-                }
-                if (colSample.meta.length) {
-                  self.sample = colSample;
-                  deferred.resolve(self.sample);
-                } else {
-                  revertToSpecific();
-                }
-              })
-              .fail(revertToSpecific);
-          } else {
-            revertToSpecific();
-          }
-        })
-        .fail(revertToSpecific);
-
-      return catalogUtils.applyCancellable(
-        self.trackedPromise(promise => {
-          self.samplePromise = promise;
-        }, new CancellableJqPromise(deferred, undefined, cancellablePromises)),
-        options
-      );
-    }
-
-    if (options && options.cachedOnly) {
-      return (
-        catalogUtils.applyCancellable(self.samplePromise, options) ||
-        $.Deferred().reject(false).promise()
-      );
-    }
-    if (options && options.refreshCache) {
-      return catalogUtils.applyCancellable(reloadSample(self, options), options);
-    }
-    return catalogUtils.applyCancellable(
-      self.samplePromise || reloadSample(self, options),
-      options
-    );
-  }
-
-  /**
-   * Gets the top aggregate UDFs for the entry if it's a table or view. It will fetch it if not cached or if the refresh option is set.
-   *
-   * @param {Object} [options]
-   * @param {boolean} [options.silenceErrors] - Default false
-   * @param {boolean} [options.cachedOnly] - Default false
-   * @param {boolean} [options.refreshCache] - Default false
-   * @param {boolean} [options.cancellable] - Default false
-   *
-   * @return {CancellableJqPromise}
-   */
-  getTopAggs(options) {
-    const self = this;
-    return getFromMultiTableCatalog(self, options, 'getTopAggs');
-  }
-
-  /**
-   * Gets the top filters for the entry if it's a table or view. It will fetch it if not cached or if the refresh option is set.
-   *
-   * @param {Object} [options]
-   * @param {boolean} [options.silenceErrors] - Default false
-   * @param {boolean} [options.cachedOnly] - Default false
-   * @param {boolean} [options.refreshCache] - Default false
-   * @param {boolean} [options.cancellable] - Default false
-   *
-   * @return {CancellableJqPromise}
-   */
-  getTopFilters(options) {
-    const self = this;
-    return getFromMultiTableCatalog(self, options, 'getTopFilters');
-  }
-
-  /**
-   * Gets the top joins for the entry if it's a table or view. It will fetch it if not cached or if the refresh option is set.
-   *
-   * @param {Object} [options]
-   * @param {boolean} [options.silenceErrors] - Default false
-   * @param {boolean} [options.cachedOnly] - Default false
-   * @param {boolean} [options.refreshCache] - Default false
-   * @param {boolean} [options.cancellable] - Default false
-   *
-   * @return {CancellableJqPromise}
-   */
-  getTopJoins(options) {
-    const self = this;
-    return getFromMultiTableCatalog(self, options, 'getTopJoins');
-  }
-}

+ 0 - 131
desktop/core/src/desktop/js/catalog/dataCatalogEntry/index.d.ts

@@ -1,131 +0,0 @@
-// 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.
-
-import CancellableJqPromise from 'api/cancellableJqPromise';
-import { UdfDetails } from 'sql/reference/types';
-
-export = DataCatalogEntry;
-
-declare class DataCatalogEntry {
-  name: string;
-  optimizerPopularity: {
-    [attr: string]: DataCatalogEntry.OptimizerPopularity | number;
-  };
-  getComment(options: DataCatalogEntry.CatalogGetOptions): CancellableJqPromise<string>;
-  getChildren(
-    options: DataCatalogEntry.CatalogGetOptions
-  ): CancellableJqPromise<DataCatalogEntry[]>;
-  getQualifiedPath(): string;
-  getResolvedComment(): string;
-  getSourceMeta(
-    options: DataCatalogEntry.CatalogGetOptions
-  ): CancellableJqPromise<DataCatalogEntry.SourceMeta>;
-  getType(): string;
-  getTopJoins(
-    options: DataCatalogEntry.CatalogGetOptions
-  ): CancellableJqPromise<DataCatalogEntry.TopJoins>;
-  getTopAggs(
-    options: DataCatalogEntry.CatalogGetOptions
-  ): CancellableJqPromise<DataCatalogEntry.TopAggs>;
-  getTopFilters(
-    options: DataCatalogEntry.CatalogGetOptions
-  ): CancellableJqPromise<DataCatalogEntry.TopFilters>;
-  hasResolvedComment(): boolean;
-  isArray(): boolean;
-  isComplex(): boolean;
-  isMap(): boolean;
-  isTable(): boolean;
-  isView(): boolean;
-  loadOptimizerPopularityForChildren(
-    options: DataCatalogEntry.CatalogGetOptions
-  ): CancellableJqPromise<DataCatalogEntry[]>;
-}
-
-declare namespace DataCatalogEntry {
-  export interface CatalogGetOptions {
-    silenceErrors?: boolean;
-    cachedOnly?: boolean;
-    refreshCache?: boolean;
-    cancellable?: boolean;
-  }
-
-  export interface OptimizerPopularity {
-    columnCount: number;
-    dbName?: string;
-    tableName?: string;
-    columnName?: string;
-  }
-
-  export interface Field {
-    type: string;
-    name: string;
-  }
-
-  export interface SourceMeta {
-    type?: string;
-    samples?: string[];
-    value?: { fields?: Field[] };
-    item?: { fields?: Field[] };
-    extended_columns?: { type: string; name: string }[];
-  }
-
-  export interface TopJoinValue {
-    totalQueryCount: number;
-    joinType: string;
-    tables: string[];
-    joinCols: { columns: string[] }[];
-    relativePopularity?: number;
-  }
-
-  export interface TopJoins {
-    values: TopJoinValue[];
-  }
-
-  export interface TopAggValue {
-    aggregateClause: string;
-    aggregateFunction: string;
-    aggregateInfo: {
-      columnName: string;
-      databaseName: string;
-      tableName: string;
-    }[];
-    function?: UdfDetails;
-    relativePopularity?: number;
-    totalQueryCount: number;
-  }
-
-  export interface TopAggs {
-    values: TopAggValue[];
-  }
-
-  export interface TopFilterValue {
-    count: number;
-    relativePopularity?: number;
-    group?: {
-      columnName: string;
-      op: string;
-      literal: string;
-    }[];
-  }
-
-  export interface TopFilters {
-    values: {
-      tableName: string;
-      count: number;
-      popularValues?: TopFilterValue[];
-    }[];
-  }
-}

+ 0 - 139
desktop/core/src/desktop/js/catalog/generalDataCatalog.js

@@ -1,139 +0,0 @@
-// 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.
-
-import $ from 'jquery';
-import localforage from 'localforage';
-
-import apiHelper from 'api/apiHelper';
-import { DataCatalog } from './dataCatalog';
-
-const STORAGE_POSTFIX = window.LOGGED_USERNAME;
-const DATA_CATALOG_VERSION = 5;
-
-class GeneralDataCatalog {
-  constructor() {
-    const self = this;
-    self.store = localforage.createInstance({
-      name: 'HueDataCatalog_' + STORAGE_POSTFIX
-    });
-
-    self.allNavigatorTagsPromise = undefined;
-  }
-
-  /**
-   * @param {Object} [options]
-   * @param {boolean} [options.silenceErrors]
-   * @param {boolean} [options.refreshCache]
-   *
-   * @return {Promise}
-   */
-  getAllNavigatorTags(options) {
-    const self = this;
-    if (
-      self.allNavigatorTagsPromise &&
-      DataCatalog.cacheEnabled() &&
-      (!options || !options.refreshCache)
-    ) {
-      return self.allNavigatorTagsPromise;
-    }
-
-    const deferred = $.Deferred();
-
-    if (!window.HAS_CATALOG) {
-      return deferred.reject().promise();
-    }
-
-    self.allNavigatorTagsPromise = deferred.promise();
-
-    const reloadAllTags = () => {
-      apiHelper
-        .fetchAllNavigatorTags({
-          silenceErrors: options && options.silenceErrors
-        })
-        .done(deferred.resolve)
-        .fail(deferred.reject);
-
-      if (window.CACHEABLE_TTL.default > 0) {
-        deferred.done(allTags => {
-          self.store.setItem('hue.dataCatalog.allNavTags', {
-            allTags: allTags,
-            hueTimestamp: Date.now(),
-            version: DATA_CATALOG_VERSION
-          });
-        });
-      }
-    };
-
-    if (
-      window.CACHEABLE_TTL.default > 0 &&
-      DataCatalog.cacheEnabled() &&
-      (!options || !options.refreshCache)
-    ) {
-      self.store
-        .getItem('hue.dataCatalog.allNavTags')
-        .then(storeEntry => {
-          if (
-            storeEntry &&
-            storeEntry.version === DATA_CATALOG_VERSION &&
-            (!storeEntry.hueTimestamp ||
-              Date.now() - storeEntry.hueTimestamp < CACHEABLE_TTL.default)
-          ) {
-            deferred.resolve(storeEntry.allTags);
-          } else {
-            reloadAllTags();
-          }
-        })
-        .catch(reloadAllTags);
-    } else {
-      reloadAllTags();
-    }
-
-    return self.allNavigatorTagsPromise;
-  }
-
-  /**
-   * @param {string[]} tagsToAdd
-   * @param {string[]} tagsToRemove
-   */
-  updateAllNavigatorTags(tagsToAdd, tagsToRemove) {
-    const self = this;
-    if (self.allNavigatorTagsPromise) {
-      self.allNavigatorTagsPromise.done(allTags => {
-        tagsToAdd.forEach(newTag => {
-          if (!allTags[newTag]) {
-            allTags[newTag] = 0;
-          }
-          allTags[newTag]++;
-        });
-        tagsToRemove.forEach(removedTag => {
-          if (!allTags[removedTag]) {
-            allTags[removedTag]--;
-            if (allTags[removedTag] === 0) {
-              delete allTags[removedTag];
-            }
-          }
-        });
-        self.store.setItem('hue.dataCatalog.allNavTags', {
-          allTags: allTags,
-          hueTimestamp: Date.now(),
-          version: DATA_CATALOG_VERSION
-        });
-      });
-    }
-  }
-}
-
-export default GeneralDataCatalog;

+ 0 - 299
desktop/core/src/desktop/js/catalog/multiTableEntry.js

@@ -1,299 +0,0 @@
-// 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.
-
-import $ from 'jquery';
-
-import catalogUtils from 'catalog/catalogUtils';
-import { getOptimizer } from './optimizer/optimizer';
-import { DataCatalog } from './dataCatalog';
-
-/**
- * Helper function to reload a Optimizer multi table attribute, like topAggs or topFilters
- *
- * @param {MultiTableEntry} multiTableEntry
- * @param {Object} [options]
- * @param {boolean} [options.silenceErrors] - Default true
- * @param {Function} promiseSetter
- * @param {Function} dataAttributeSetter
- * @param {Function} apiHelperFunction
- * @return {CancellableJqPromise}
- */
-const genericOptimizerReload = function (
-  multiTableEntry,
-  options,
-  promiseSetter,
-  dataAttributeSetter,
-  apiHelperFunction
-) {
-  if (multiTableEntry.dataCatalog.canHaveOptimizerMeta()) {
-    return multiTableEntry.trackedPromise(
-      promiseSetter,
-      catalogUtils.fetchAndSave(apiHelperFunction, dataAttributeSetter, multiTableEntry, options)
-    );
-  }
-  const promise = $.Deferred().reject();
-  promiseSetter(promise);
-  return promise;
-};
-
-/**
- * Helper function to get a Optimizer multi table attribute, like topAggs or topFilters
- *
- * @param {MultiTableEntry} multiTableEntry
- * @param {Object} [options]
- * @param {boolean} [options.silenceErrors] - Default false
- * @param {boolean} [options.refreshCache] - Default false
- * @param {boolean} [options.cachedOnly] - Default false
- * @param {boolean} [options.cancellable] - Default false
- * @param {Function} promiseSetter
- * @param {Function} promiseGetter
- * @param {Function} dataAttributeSetter
- * @param {Function} apiHelperFunction
- * @return {CancellableJqPromise}
- */
-const genericOptimizerGet = function (
-  multiTableEntry,
-  options,
-  promiseSetter,
-  promiseGetter,
-  dataAttributeSetter,
-  apiHelperFunction
-) {
-  if (DataCatalog.cacheEnabled() && options && options.cachedOnly) {
-    return (
-      catalogUtils.applyCancellable(promiseGetter(), options) ||
-      $.Deferred().reject(false).promise()
-    );
-  }
-  if (!DataCatalog.cacheEnabled() || (options && options.refreshCache)) {
-    return catalogUtils.applyCancellable(
-      genericOptimizerReload(
-        multiTableEntry,
-        options,
-        promiseSetter,
-        dataAttributeSetter,
-        apiHelperFunction
-      ),
-      options
-    );
-  }
-  return catalogUtils.applyCancellable(
-    promiseGetter() ||
-      genericOptimizerReload(
-        multiTableEntry,
-        options,
-        promiseSetter,
-        dataAttributeSetter,
-        apiHelperFunction
-      ),
-    options
-  );
-};
-
-class MultiTableEntry {
-  /**
-   *
-   * @param {Object} options
-   * @param {string} options.identifier
-   * @param {DataCatalog} options.dataCatalog
-   * @param {string[][]} options.paths
-   * @constructor
-   */
-  constructor(options) {
-    const self = this;
-    self.identifier = options.identifier;
-    self.dataCatalog = options.dataCatalog;
-    self.paths = options.paths;
-
-    self.topAggs = undefined;
-    self.topAggsPromise = undefined;
-
-    self.topColumns = undefined;
-    self.topColumnsPromise = undefined;
-
-    self.topFilters = undefined;
-    self.topFiltersPromise = undefined;
-
-    self.topJoins = undefined;
-    self.topJoinsPromise = undefined;
-  }
-
-  /**
-   * Save the multi table entry to cache
-   *
-   * @return {Promise}
-   */
-  save() {
-    const self = this;
-    window.clearTimeout(self.saveTimeout);
-    return self.dataCatalog.persistMultiTableEntry(self);
-  }
-
-  /**
-   * Save the multi table entry at a later point of time
-   */
-  saveLater() {
-    const self = this;
-    if (CACHEABLE_TTL.default > 0) {
-      window.clearTimeout(self.saveTimeout);
-      self.saveTimeout = window.setTimeout(() => {
-        self.save();
-      }, 1000);
-    }
-  }
-  /**
-   * Helper function that ensure that cancellable promises are not tracked anymore when cancelled
-   *
-   * @param {Function} promiseSetter - The promise attribute to use
-   * @param {CancellableJqPromise} cancellableJqPromise
-   */
-  trackedPromise(promiseSetter, cancellableJqPromise) {
-    promiseSetter(cancellableJqPromise);
-    return cancellableJqPromise.fail(() => {
-      if (cancellableJqPromise.cancelled) {
-        delete promiseSetter(undefined);
-      }
-    });
-  }
-
-  /**
-   * Returns the dialect of this entry.
-   *
-   * @return {string} - 'impala', 'hive', 'solr', etc.
-   */
-  getDialect() {
-    return this.getConnector().dialect || this.getConnector().id; // .id for editor v1
-  }
-
-  /**
-   * Returns the connector for this entry
-   *
-   * @return {Connector}
-   */
-  getConnector() {
-    return this.dataCatalog.connector;
-  }
-
-  /**
-   * Gets the top aggregate UDFs for the entry. It will fetch it if not cached or if the refresh option is set.
-   *
-   * @param {Object} [options]
-   * @param {boolean} [options.silenceErrors] - Default false
-   * @param {boolean} [options.cachedOnly] - Default false
-   * @param {boolean} [options.refreshCache] - Default false
-   * @param {boolean} [options.cancellable] - Default false
-   *
-   * @return {CancellableJqPromise}
-   */
-  getTopAggs(options) {
-    const optimizer = getOptimizer(this.dataCatalog.connector);
-    return genericOptimizerGet(
-      this,
-      options,
-      promise => {
-        this.topAggsPromise = promise;
-      },
-      () => this.topAggsPromise,
-      val => {
-        this.topAggs = val;
-      },
-      optimizer.fetchTopAggs.bind(optimizer)
-    );
-  }
-
-  /**
-   * Gets the top columns for the entry. It will fetch it if not cached or if the refresh option is set.
-   *
-   * @param {Object} [options]
-   * @param {boolean} [options.silenceErrors] - Default false
-   * @param {boolean} [options.cachedOnly] - Default false
-   * @param {boolean} [options.refreshCache] - Default false
-   * @param {boolean} [options.cancellable] - Default false
-   *
-   * @return {CancellableJqPromise}
-   */
-  getTopColumns(options) {
-    const optimizer = getOptimizer(this.dataCatalog.connector);
-    return genericOptimizerGet(
-      this,
-      options,
-      promise => {
-        this.topColumnsPromise = promise;
-      },
-      () => this.topColumnsPromise,
-      val => {
-        this.topColumns = val;
-      },
-      optimizer.fetchTopColumns.bind(optimizer)
-    );
-  }
-
-  /**
-   * Gets the top filters for the entry. It will fetch it if not cached or if the refresh option is set.
-   *
-   * @param {Object} [options]
-   * @param {boolean} [options.silenceErrors] - Default false
-   * @param {boolean} [options.cachedOnly] - Default false
-   * @param {boolean} [options.refreshCache] - Default false
-   * @param {boolean} [options.cancellable] - Default false
-   *
-   * @return {CancellableJqPromise}
-   */
-  getTopFilters(options) {
-    const optimizer = getOptimizer(this.dataCatalog.connector);
-    return genericOptimizerGet(
-      this,
-      options,
-      promise => {
-        this.topFiltersPromise = promise;
-      },
-      () => this.topFiltersPromise,
-      val => {
-        this.topFilters = val;
-      },
-      optimizer.fetchTopFilters.bind(optimizer)
-    );
-  }
-
-  /**
-   * Gets the top joins for the entry. It will fetch it if not cached or if the refresh option is set.
-   *
-   * @param {Object} [options]
-   * @param {boolean} [options.silenceErrors] - Default false
-   * @param {boolean} [options.cachedOnly] - Default false
-   * @param {boolean} [options.refreshCache] - Default false
-   * @param {boolean} [options.cancellable] - Default false
-   *
-   * @return {CancellableJqPromise}
-   */
-  getTopJoins(options) {
-    const optimizer = getOptimizer(this.dataCatalog.connector);
-    return genericOptimizerGet(
-      this,
-      options,
-      promise => {
-        this.topJoinsPromise = promise;
-      },
-      () => this.topJoinsPromise,
-      val => {
-        this.topJoins = val;
-      },
-      optimizer.fetchTopJoins.bind(optimizer)
-    );
-  }
-}
-
-export default MultiTableEntry;

+ 2 - 2
desktop/core/src/desktop/js/catalog/optimizer/localStrategy.js

@@ -89,9 +89,9 @@ export default class LocalStrategy extends BaseStrategy {
             };
             deferred.resolve(data);
           })
-          .fail(deferred.reject);
+          .catch(deferred.reject);
       })
-      .fail(deferred.reject);
+      .catch(deferred.reject);
 
     return deferred.promise();
   }

+ 10 - 8
desktop/core/src/desktop/js/jquery/plugins/jquery.hiveautocomplete.js

@@ -199,7 +199,7 @@ Plugin.prototype.init = function () {
               compute: compute,
               path: path
             })
-            .done(childEntries => {
+            .then(childEntries => {
               if (
                 childEntries.some(childEntry => {
                   return childEntry.name === target;
@@ -207,7 +207,8 @@ Plugin.prototype.init = function () {
               ) {
                 onPathChange($el.val());
               }
-            });
+            })
+            .catch(() => {});
         });
       }, 500);
     }
@@ -320,13 +321,14 @@ Plugin.prototype.init = function () {
           compute: compute,
           path: []
         })
-        .done(dbEntries => {
+        .then(dbEntries => {
           callback(
             $.map(dbEntries, entry => {
               return entry.name;
             })
           );
-        });
+        })
+        .catch(() => {});
     });
   };
 
@@ -340,8 +342,8 @@ Plugin.prototype.init = function () {
           connector: { id: self.options.apiHelperType },
           path: [database]
         })
-        .done(entry => {
-          entry.getSourceMeta().done(callback);
+        .then(entry => {
+          entry.getSourceMeta().then(callback);
         });
     });
   };
@@ -356,8 +358,8 @@ Plugin.prototype.init = function () {
           connector: { id: self.options.apiHelperType },
           path: [database, table]
         })
-        .done(entry => {
-          entry.getSourceMeta().done(callback);
+        .then(entry => {
+          entry.getSourceMeta().then(callback);
         });
     });
   };

+ 9 - 9
desktop/core/src/desktop/js/ko/bindings/ace/aceLocationHandler.js

@@ -199,10 +199,10 @@ class AceLocationHandler {
                           return identifier.name;
                         })
                       })
-                      .done(entry => {
+                      .then(entry => {
                         entry
                           .getSourceMeta({ cachedOnly: true, silenceErrors: true })
-                          .done(sourceMeta => {
+                          .then(sourceMeta => {
                             if (sourceMeta && sourceMeta.extended_columns) {
                               sourceMeta.extended_columns.every(col => {
                                 if (col.name.toLowerCase() === colName) {
@@ -380,7 +380,7 @@ class AceLocationHandler {
               .resolveCatalogEntry({
                 temporaryOnly: self.snippet.autocompleteSettings.temporaryOnly
               })
-              .done(entry => {
+              .then(entry => {
                 huePubSub.publish('context.popover.show', {
                   data: {
                     type: 'catalogEntry',
@@ -391,13 +391,13 @@ class AceLocationHandler {
                   source: source
                 });
               })
-              .fail(() => {
+              .catch(() => {
                 token.notFound = true;
               });
           } else if (token.parseLocation && !token.notFound) {
             // Asterisk, function etc.
             if (token.parseLocation.type === 'file') {
-              AssistStorageEntry.getEntry(token.parseLocation.path).done(entry => {
+              AssistStorageEntry.getEntry(token.parseLocation.path).then(entry => {
                 entry.open(true);
                 huePubSub.publish('context.popover.show', {
                   data: {
@@ -871,8 +871,8 @@ class AceLocationHandler {
         silenceErrors: true,
         cachedOnly: true
       })
-      .done(deferred.resolve)
-      .fail(() => {
+      .then(deferred.resolve)
+      .catch(() => {
         deferred.reject([]);
       });
     return deferred;
@@ -989,7 +989,7 @@ class AceLocationHandler {
                 cachedOnly: true,
                 silenceErrors: true
               })
-              .done(entries => {
+              .then(entries => {
                 const containsColumn = entries.some(entry => {
                   return sqlUtils.identifierEquals(entry.name, location.identifierChain[0].name);
                 });
@@ -1006,7 +1006,7 @@ class AceLocationHandler {
                   promise.resolve();
                 }
               })
-              .fail(promise.resolve);
+              .catch(promise.resolve);
           } else if (tablesToGo.length > 0) {
             findIdentifierChainInTable(tablesToGo);
           } else {

+ 1 - 1
desktop/core/src/desktop/js/ko/bindings/ko.sqlContextPopover.js

@@ -60,7 +60,7 @@ ko.bindingHandlers.sqlContextPopover = {
               dialect: options.sourceType
             };
           }
-          dataCatalog.getEntry(options).done(entry => {
+          dataCatalog.getEntry(options).then(entry => {
             const $source = $(element);
             const offset = $source.offset();
             if (options.offset) {

+ 1 - 1
desktop/core/src/desktop/js/ko/bindings/ko.storageContextPopover.js

@@ -46,7 +46,7 @@ ko.bindingHandlers.storageContextPopover = {
       () => {
         return function () {
           const options = valueAccessor();
-          AssistStorageEntry.getEntry(options.path, options.type).done(entry => {
+          AssistStorageEntry.getEntry(options.path, options.type).then(entry => {
             const $source = $(element);
             const offset = $source.offset();
 

+ 10 - 8
desktop/core/src/desktop/js/ko/components/assist/assistDbEntry.js

@@ -343,7 +343,7 @@ class AssistDbEntry {
       if (!sourceMeta.notFound) {
         self.catalogEntry
           .getChildren({ silenceErrors: self.navigationSettings.rightAssist })
-          .done(catalogEntries => {
+          .then(catalogEntries => {
             self.hasErrors(false);
             self.loading(false);
             self.loaded = true;
@@ -367,16 +367,18 @@ class AssistDbEntry {
               callback();
             }
           })
-          .fail(() => {
+          .catch(() => {
             self.loading(false);
             self.loaded = true;
             self.hasErrors(true);
           });
 
         if (!self.assistDbNamespace.nonSqlType) {
-          self.catalogEntry.loadNavigatorMetaForChildren({
-            silenceErrors: self.navigationSettings.rightAssist
-          });
+          self.catalogEntry
+            .loadNavigatorMetaForChildren({
+              silenceErrors: self.navigationSettings.rightAssist
+            })
+            .finally(() => {});
         }
       } else {
         self.hasErrors(true);
@@ -401,7 +403,7 @@ class AssistDbEntry {
       (self.catalogEntry.isTable() || self.catalogEntry.isDatabase()) &&
       !self.assistDbNamespace.nonSqlType
     ) {
-      self.catalogEntry.loadOptimizerPopularityForChildren({ silenceErrors: true }).done(() => {
+      self.catalogEntry.loadOptimizerPopularityForChildren({ silenceErrors: true }).then(() => {
         loadEntriesDeferred.done(() => {
           if (!self.hasErrors()) {
             self.entries().forEach(entry => {
@@ -422,8 +424,8 @@ class AssistDbEntry {
 
     self.catalogEntry
       .getSourceMeta({ silenceErrors: self.navigationSettings.rightAssist })
-      .done(successCallback)
-      .fail(errorCallback);
+      .then(successCallback)
+      .catch(errorCallback);
   }
 
   /**

+ 8 - 6
desktop/core/src/desktop/js/ko/components/assist/assistDbNamespace.js

@@ -118,7 +118,7 @@ class AssistDbNamespace {
       }
 
       if (window.HAS_OPTIMIZER && db && !db.popularityIndexSet && !self.nonSqlType) {
-        db.catalogEntry.loadOptimizerPopularityForChildren({ silenceErrors: true }).done(() => {
+        db.catalogEntry.loadOptimizerPopularityForChildren({ silenceErrors: true }).then(() => {
           const applyPopularity = () => {
             db.entries().forEach(entry => {
               if (
@@ -225,11 +225,11 @@ class AssistDbNamespace {
           path: [],
           definition: { type: 'source' }
         })
-        .done(catalogEntry => {
+        .then(catalogEntry => {
           self.catalogEntry = catalogEntry;
           self.catalogEntry
             .getChildren({ silenceErrors: self.navigationSettings.rightAssist })
-            .done(databaseEntries => {
+            .then(databaseEntries => {
               self.dbIndex = {};
               let hasNavMeta = false;
               const dbs = [];
@@ -253,7 +253,9 @@ class AssistDbNamespace {
               });
 
               if (!hasNavMeta && !self.nonSqlType) {
-                self.catalogEntry.loadNavigatorMetaForChildren({ silenceErrors: true });
+                try {
+                  self.catalogEntry.loadNavigatorMetaForChildren({ silenceErrors: true });
+                } catch (err) {}
               }
               self.databases(dbs);
 
@@ -261,10 +263,10 @@ class AssistDbNamespace {
                 callback();
               }
             })
-            .fail(() => {
+            .catch(() => {
               self.hasErrors(true);
             })
-            .always(() => {
+            .finally(() => {
               self.loaded(true);
               self.loadedDeferred.resolve();
               self.loading(false);

+ 2 - 2
desktop/core/src/desktop/js/ko/components/assist/ko.assistDashboardPanel.js

@@ -94,7 +94,7 @@ class AssistDashboardPanel {
           path: [fakeParentName],
           definition: { type: 'database' }
         })
-        .done(fakeDbCatalogEntry => {
+        .then(fakeDbCatalogEntry => {
           const assistFakeDb = new AssistDbEntry(
             fakeDbCatalogEntry,
             null,
@@ -114,7 +114,7 @@ class AssistDashboardPanel {
               ],
               definition: { type: 'table' }
             })
-            .done(collectionCatalogEntry => {
+            .then(collectionCatalogEntry => {
               const collectionEntry = new AssistDbEntry(
                 collectionCatalogEntry,
                 assistFakeDb,

+ 1 - 1
desktop/core/src/desktop/js/ko/components/assist/ko.assistDbPanel.js

@@ -642,7 +642,7 @@ class AssistDbPanel {
                 connector: { id: 'solr' },
                 path: []
               })
-              .done(entry => {
+              .then(entry => {
                 entry.clearCache({ cascade: true });
               });
           }

+ 12 - 12
desktop/core/src/desktop/js/ko/components/assist/ko.assistEditorContextPanel.js

@@ -24,7 +24,7 @@ import componentUtils from 'ko/components/componentUtils';
 import dataCatalog from 'catalog/dataCatalog';
 import huePubSub from 'utils/huePubSub';
 import I18n from 'utils/i18n';
-import DataCatalogEntry from 'catalog/dataCatalogEntry';
+import DataCatalogEntry from 'catalog/DataCatalogEntry';
 
 const TEMPLATE =
   ASSIST_TABLE_TEMPLATES +
@@ -420,7 +420,7 @@ class AssistEditorContextPanel {
                     path: [database],
                     definition: { type: 'database' }
                   })
-                  .done(catalogEntry => {
+                  .then(catalogEntry => {
                     databaseIndex[database] = new AssistDbEntry(
                       catalogEntry,
                       null,
@@ -432,7 +432,7 @@ class AssistEditorContextPanel {
                     updateTables = true;
                     dbDeferred.resolve(databaseIndex[database]);
                   })
-                  .fail(() => {
+                  .catch(() => {
                     dbDeferred.reject();
                   });
               }
@@ -441,7 +441,7 @@ class AssistEditorContextPanel {
                 .done(dbEntry => {
                   dbEntry.catalogEntry
                     .getChildren({ silenceErrors: true })
-                    .done(tableEntries => {
+                    .then(tableEntries => {
                       const tableName =
                         location.identifierChain[location.identifierChain.length - 1].name;
                       const found = tableEntries.some(tableEntry => {
@@ -483,7 +483,7 @@ class AssistEditorContextPanel {
                           }
                           self.reloading(true);
                           huePubSub.subscribeOnce('data.catalog.entry.refreshed', data => {
-                            data.entry.getSourceMeta({ silenceErrors: true }).always(() => {
+                            data.entry.getSourceMeta({ silenceErrors: true }).finally(() => {
                               self.reloading(false);
                             });
                           });
@@ -494,10 +494,10 @@ class AssistEditorContextPanel {
                               connector: activeLocations.connector,
                               path: []
                             })
-                            .done(sourceEntry => {
+                            .then(sourceEntry => {
                               sourceEntry
                                 .getChildren()
-                                .done(dbEntries => {
+                                .then(dbEntries => {
                                   let clearPromise;
                                   // Clear the database first if it exists without cascade
                                   const hasDb = dbEntries.some(dbEntry => {
@@ -516,15 +516,15 @@ class AssistEditorContextPanel {
                                       cascade: false
                                     });
                                   }
-                                  clearPromise.fail(() => {
+                                  clearPromise.catch(() => {
                                     self.reloading(false);
                                   });
                                 })
-                                .fail(() => {
+                                .catch(() => {
                                   self.reloading(false);
                                 });
                             })
-                            .fail(() => {
+                            .catch(() => {
                               self.reloading(false);
                             });
                         };
@@ -549,9 +549,9 @@ class AssistEditorContextPanel {
                         tableDeferred.resolve(missingEntry);
                       }
                     })
-                    .fail(tableDeferred.reject);
+                    .catch(tableDeferred.reject);
                 })
-                .fail(tableDeferred.reject);
+                .catch(tableDeferred.reject);
             }
           }
         });

+ 8 - 5
desktop/core/src/desktop/js/ko/components/assist/ko.assistKey.js

@@ -82,11 +82,14 @@ class AssistKey {
         if (this.onForeignKeyClick) {
           // Ensure definition is loaded
           if (!entry.definition) {
-            entry.getParent().then(parentEntry => {
-              parentEntry.getChildren().then(() => {
-                this.onForeignKeyClick(entry);
-              });
-            });
+            entry
+              .getParent()
+              .then(parentEntry => {
+                parentEntry.getChildren().then(() => {
+                  this.onForeignKeyClick(entry);
+                });
+              })
+              .catch(() => {});
           } else {
             this.onForeignKeyClick(entry);
           }

+ 4 - 4
desktop/core/src/desktop/js/ko/components/contextPopover/asteriskContextTabs.js

@@ -106,10 +106,10 @@ class AsteriskData {
             connector: connector,
             path: path
           })
-          .done(entry => {
+          .then(entry => {
             entry
               .getSourceMeta({ silenceErrors: true })
-              .done(sourceMeta => {
+              .then(sourceMeta => {
                 if (typeof sourceMeta.extended_columns !== 'undefined') {
                   const newColumns = [];
                   sourceMeta.extended_columns.forEach(column => {
@@ -135,9 +135,9 @@ class AsteriskData {
                 }
                 fetchDeferred.resolve();
               })
-              .fail(fetchDeferred.reject);
+              .catch(fetchDeferred.reject);
           })
-          .fail(fetchDeferred.reject);
+          .catch(fetchDeferred.reject);
       }
     });
 

+ 9 - 9
desktop/core/src/desktop/js/ko/components/contextPopover/dataCatalogContext.js

@@ -66,7 +66,7 @@ class DataCatalogContext {
                   path: this.path,
                   temporaryOnly: self.catalogEntry().isTemporary
                 })
-                .done(self.catalogEntry);
+                .then(self.catalogEntry);
             }
           });
         }
@@ -83,7 +83,7 @@ class DataCatalogContext {
 
   refresh() {
     const self = this;
-    self.catalogEntry().clearCache({ cascade: true }).always(self.load.bind(self));
+    self.catalogEntry().clearCache({ cascade: true }).finally(self.load.bind(self));
   }
 
   load() {
@@ -99,10 +99,10 @@ class DataCatalogContext {
       self
         .catalogEntry()
         .getSourceMeta({ cancellable: true })
-        .fail(() => {
+        .catch(() => {
           self.hasErrors(true);
         })
-        .always(() => {
+        .finally(() => {
           self.loading(false);
         })
     );
@@ -119,21 +119,21 @@ class DataCatalogContext {
             silenceErrors: true,
             cancellable: true
           })
-          .done(analysis => {
+          .then(analysis => {
             const found =
               analysis.properties &&
               analysis.properties.some(property => {
                 if (property.col_name.toLowerCase() === 'view original text:') {
                   apiHelper
                     .formatSql({ statements: property.data_type })
-                    .done(formatResponse => {
+                    .then(formatResponse => {
                       if (formatResponse.status === 0) {
                         viewSqlDeferred.resolve(formatResponse.formatted_statements);
                       } else {
                         viewSqlDeferred.resolve(property.data_type);
                       }
                     })
-                    .fail(() => {
+                    .catch(() => {
                       viewSqlDeferred.resolve(property.data_type);
                     });
                   return true;
@@ -144,14 +144,14 @@ class DataCatalogContext {
             }
             self.analysis(analysis);
           })
-          .fail(viewSqlDeferred.reject)
+          .catch(viewSqlDeferred.reject)
       );
     } else {
       viewSqlDeferred.reject();
     }
 
     self.activePromises.push(
-      self.catalogEntry().getComment({ silenceErrors: true, cancellable: true }).done(self.comment)
+      self.catalogEntry().getComment({ silenceErrors: true, cancellable: true }).then(self.comment)
     );
 
     $.when.apply($, self.activePromises).always(() => {

+ 5 - 2
desktop/core/src/desktop/js/ko/components/contextPopover/ko.contextPopover.js

@@ -16,6 +16,7 @@
 
 import $ from 'jquery';
 import * as ko from 'knockout';
+import { CancellablePromise } from '../../../api/cancellablePromise';
 
 import AsteriskContextTabs from './asteriskContextTabs';
 import CollectionContextTabs from './collectionContextTabs';
@@ -939,9 +940,11 @@ class SqlContextContentsGlobalSearch {
             path: path,
             definition: { type: params.data.type.toLowerCase() }
           })
-          .done(catalogEntry => {
+          .then(catalogEntry => {
             catalogEntry.navigatorMeta = params.data;
-            catalogEntry.navigatorMetaPromise = $.Deferred().resolve(catalogEntry.navigatorMeta);
+            catalogEntry.navigatorMetaPromise = CancellablePromise.resolve(
+              catalogEntry.navigatorMeta
+            );
             catalogEntry.saveLater();
             self.contents(new DataCatalogContext({ popover: self, catalogEntry: catalogEntry }));
           });

+ 1 - 1
desktop/core/src/desktop/js/ko/components/contextPopover/storageContext.js

@@ -80,7 +80,7 @@ class StorageContext {
 
   goHome() {
     const self = this;
-    AssistStorageEntry.getEntry(window.USER_HOME_DIR, self.storageEntry().type).done(
+    AssistStorageEntry.getEntry(window.USER_HOME_DIR, self.storageEntry().type).then(
       self.storageEntry
     );
   }

+ 53 - 52
desktop/core/src/desktop/js/ko/components/ko.catalogEntriesList.js

@@ -290,8 +290,8 @@ class SampleEnrichedEntry {
       self
         .catalogEntry()
         .setComment(self.comment())
-        .done(self.comment)
-        .fail(() => {
+        .then(self.comment)
+        .catch(() => {
           self.comment(self.catalogEntry().getResolvedComment());
         });
     }
@@ -442,8 +442,10 @@ class CatalogEntriesList {
 
       const childPromise = self
         .catalogEntry()
-        .getChildren({ silenceErrors: true, cancellable: true })
-        .done(childEntries => {
+        .getChildren({ silenceErrors: true, cancellable: true });
+
+      childPromise
+        .then(childEntries => {
           const entries = $.map(childEntries, (entry, index) => {
             return new SampleEnrichedEntry(index, entry, onClick, onRowClick);
           });
@@ -451,60 +453,60 @@ class CatalogEntriesList {
           self.entries(entries);
           entriesAddedDeferred.resolve(entries);
         })
-        .fail(() => {
+        .catch(() => {
           self.hasErrors(true);
           entriesAddedDeferred.reject();
         })
-        .always(() => {
+        .finally(() => {
           self.loading(false);
         });
 
       if (self.catalogEntry().isTableOrView()) {
         const joinsPromise = self
           .catalogEntry()
-          .getTopJoins({ silenceErrors: true, cancellable: true })
-          .done(topJoins => {
-            if (topJoins && topJoins.values && topJoins.values.length) {
-              entriesAddedDeferred.done(entries => {
-                const entriesIndex = {};
-                entries.forEach(entry => {
-                  entriesIndex[entry.catalogEntry().path.join('.').toLowerCase()] = {
-                    joinColumnIndex: {},
-                    entry: entry
-                  };
-                });
-                topJoins.values.forEach(topJoin => {
-                  topJoin.joinCols.forEach(topJoinCols => {
-                    if (topJoinCols.columns.length === 2) {
-                      if (entriesIndex[topJoinCols.columns[0].toLowerCase()]) {
-                        entriesIndex[topJoinCols.columns[0].toLowerCase()].joinColumnIndex[
-                          topJoinCols.columns[1].toLowerCase()
-                        ] = topJoinCols.columns[1];
-                      } else if (entriesIndex[topJoinCols.columns[1].toLowerCase()]) {
-                        entriesIndex[topJoinCols.columns[1].toLowerCase()].joinColumnIndex[
-                          topJoinCols.columns[0].toLowerCase()
-                        ] = topJoinCols.columns[0];
-                      }
+          .getTopJoins({ silenceErrors: true, cancellable: true });
+        joinsPromise.then(topJoins => {
+          if (topJoins && topJoins.values && topJoins.values.length) {
+            entriesAddedDeferred.then(entries => {
+              const entriesIndex = {};
+              entries.forEach(entry => {
+                entriesIndex[entry.catalogEntry().path.join('.').toLowerCase()] = {
+                  joinColumnIndex: {},
+                  entry: entry
+                };
+              });
+              topJoins.values.forEach(topJoin => {
+                topJoin.joinCols.forEach(topJoinCols => {
+                  if (topJoinCols.columns.length === 2) {
+                    if (entriesIndex[topJoinCols.columns[0].toLowerCase()]) {
+                      entriesIndex[topJoinCols.columns[0].toLowerCase()].joinColumnIndex[
+                        topJoinCols.columns[1].toLowerCase()
+                      ] = topJoinCols.columns[1];
+                    } else if (entriesIndex[topJoinCols.columns[1].toLowerCase()]) {
+                      entriesIndex[topJoinCols.columns[1].toLowerCase()].joinColumnIndex[
+                        topJoinCols.columns[0].toLowerCase()
+                      ] = topJoinCols.columns[0];
                     }
-                  });
-                });
-                Object.keys(entriesIndex).forEach(key => {
-                  if (Object.keys(entriesIndex[key].joinColumnIndex).length) {
-                    entriesIndex[key].entry.joinColumns(
-                      Object.keys(entriesIndex[key].joinColumnIndex)
-                    );
                   }
                 });
               });
-            }
-          });
+              Object.keys(entriesIndex).forEach(key => {
+                if (Object.keys(entriesIndex[key].joinColumnIndex).length) {
+                  entriesIndex[key].entry.joinColumns(
+                    Object.keys(entriesIndex[key].joinColumnIndex)
+                  );
+                }
+              });
+            });
+          }
+        });
         self.cancellablePromises.push(joinsPromise);
       }
 
       const navMetaPromise = self
         .catalogEntry()
         .loadNavigatorMetaForChildren({ silenceErrors: true, cancellable: true })
-        .always(() => {
+        .finally(() => {
           self.loadingNav(false);
         });
 
@@ -515,9 +517,9 @@ class CatalogEntriesList {
         self
           .catalogEntry()
           .loadOptimizerPopularityForChildren({ silenceErrors: true, cancellable: true })
-          .done(popularEntries => {
+          .then(popularEntries => {
             if (popularEntries.length) {
-              childPromise.done(() => {
+              childPromise.then(() => {
                 const entryIndex = {};
                 self.entries().forEach(entry => {
                   entryIndex[entry.catalogEntry().name] = entry;
@@ -562,16 +564,15 @@ class CatalogEntriesList {
 
         const fetchSamples = function () {
           window.clearInterval(self.fetchSampleTimeout);
-          self.lastSamplePromise = self
-            .catalogEntry()
-            .getSample({
-              silenceErrors: true,
-              cancellable: true,
-              refreshCache: !firstSampleFetch
-            })
-            .done(sample => {
+          self.lastSamplePromise = self.catalogEntry().getSample({
+            silenceErrors: true,
+            cancellable: true,
+            refreshCache: !firstSampleFetch
+          });
+          self.lastSamplePromise
+            .then(sample => {
               childPromise
-                .done(() => {
+                .then(() => {
                   if (sample.meta && sample.meta.length && sample.data && sample.data.length) {
                     const entryIndex = {};
                     self.entries().forEach(entry => {
@@ -595,7 +596,7 @@ class CatalogEntriesList {
                     }
                   }
                 })
-                .always(() => {
+                .finally(() => {
                   self.loadingSamples(false);
                   firstSampleFetch = false;
                   if (self.refreshSampleInterval && self.sampleRefreshEnabled()) {
@@ -606,7 +607,7 @@ class CatalogEntriesList {
                   }
                 });
             })
-            .fail(() => {
+            .catch(() => {
               self.loadingSamples(false);
             });
         };

+ 7 - 5
desktop/core/src/desktop/js/ko/components/ko.catalogEntriesList.test.js

@@ -14,8 +14,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import $ from 'jquery';
 import { koSetup } from 'jest/koTestUtils';
+import { CancellablePromise } from '../../api/cancellablePromise';
 import { NAME } from './ko.catalogEntriesList';
 
 import 'ko/components/ko.inlineAutocomplete';
@@ -31,16 +31,18 @@ describe('ko.catalogEntriesList.js', () => {
         isField: () => true,
         isComplex: () => false,
         isDatabase: () => false,
-        getChildren: () => $.Deferred().resolve([]),
-        getSample: () => $.Deferred().reject(),
+        getChildren: () => CancellablePromise.resolve([]),
+        getSample: () => CancellablePromise.reject(),
         getDialect: () => 'impala',
-        loadNavigatorMetaForChildren: () => $.Deferred().reject(),
-        loadOptimizerPopularityForChildren: () => $.Deferred().reject(),
+        loadNavigatorMetaForChildren: () => CancellablePromise.reject(),
+        loadOptimizerPopularityForChildren: () => CancellablePromise.reject(),
         isTableOrView: () => false,
         isSource: () => false
       }
     });
 
+    await setup.waitForKoUpdate();
+
     expect(element.innerHTML).toMatchSnapshot();
   });
 });

+ 4 - 4
desktop/core/src/desktop/js/ko/components/ko.contextSelector.js

@@ -413,20 +413,20 @@ HueContextSelector.prototype.reloadDatabases = function () {
               path: [],
               definition: { type: 'source' }
             })
-            .done(sourceEntry => {
+            .then(sourceEntry => {
               sourceEntry
                 .getChildren({ silenceErrors: true })
-                .done(databaseEntries => {
+                .then(databaseEntries => {
                   const databaseNames = [];
                   databaseEntries.forEach(databaseEntry => {
                     databaseNames.push(databaseEntry.name);
                   });
                   self.availableDatabases(databaseNames);
                 })
-                .fail(() => {
+                .catch(() => {
                   self.availableDatabases([]);
                 })
-                .always(() => {
+                .finally(() => {
                   let lastSelectedDb = getFromLocalStorage(
                     'assist_' +
                       ko.unwrap(self.connector).id +

+ 2 - 2
desktop/core/src/desktop/js/ko/components/ko.executionAnalysis.js

@@ -163,11 +163,11 @@ class ExecutionAnalysis {
         compute: compute,
         queryId: queryId
       })
-      .done(response => {
+      .then(response => {
         const analysis = response.query;
         self.analysis(analysis);
       })
-      .always(() => {
+      .finally(() => {
         self.loading(false);
       });
   }

+ 24 - 26
desktop/core/src/desktop/js/ko/components/ko.fieldSamples.js

@@ -179,32 +179,30 @@ class FieldSamples {
     window.clearTimeout(self.refreshSampleTimeout);
     self.cancelRunningQueries();
     self.loadingSamples(true);
-    self.cancellablePromises.push(
-      self
-        .catalogEntry()
-        .getSample({
-          silenceErrors: true,
-          cancellable: true,
-          refreshCache: refreshCache,
-          operation: self.operation().type
-        })
-        .done(samples => {
-          if (samples.data && samples.data.length) {
-            self.columnSamples(samples.data);
-          }
-        })
-        .fail(() => {
-          self.hasErrors(true);
-        })
-        .always(() => {
-          self.loadingSamples(false);
-          if (self.refreshSampleTimeout && self.refreshSampleEnabled()) {
-            self.refreshSampleTimeout = window.setTimeout(() => {
-              self.loadSamples(true);
-            }, self.refreshSampleInterval);
-          }
-        })
-    );
+    const samplePromise = self.catalogEntry().getSample({
+      silenceErrors: true,
+      cancellable: true,
+      refreshCache: refreshCache,
+      operation: self.operation().type
+    });
+    self.cancellablePromises.push(samplePromise);
+    samplePromise
+      .then(samples => {
+        if (samples.data && samples.data.length) {
+          self.columnSamples(samples.data);
+        }
+      })
+      .catch(() => {
+        self.hasErrors(true);
+      })
+      .finally(() => {
+        self.loadingSamples(false);
+        if (self.refreshSampleTimeout && self.refreshSampleEnabled()) {
+          self.refreshSampleTimeout = window.setTimeout(() => {
+            self.loadSamples(true);
+          }, self.refreshSampleInterval);
+        }
+      });
   }
 
   cancelRunningQueries() {

+ 6 - 7
desktop/core/src/desktop/js/ko/components/ko.fieldSamples.test.js

@@ -14,9 +14,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import $ from 'jquery';
-
 import { koSetup } from 'jest/koTestUtils';
+import { CancellablePromise } from '../../api/cancellablePromise';
 import { NAME } from './ko.fieldSamples';
 
 import 'ko/components/ko.inlineAutocomplete';
@@ -30,15 +29,15 @@ describe('ko.fieldSamples.js', () => {
       catalogEntry: {
         isField: () => true,
         isComplex: () => false,
-        getChildren: () => $.Deferred().resolve([]),
-        getSample: () => $.Deferred().reject(),
-        loadNavigatorMetaForChildren: () => $.Deferred().reject(),
-        loadOptimizerPopularityForChildren: () => $.Deferred().reject(),
+        getChildren: () => CancellablePromise.resolve([]),
+        getSample: () => CancellablePromise.reject(),
+        loadNavigatorMetaForChildren: () => CancellablePromise.reject(),
+        loadOptimizerPopularityForChildren: () => CancellablePromise.reject(),
         isTableOrView: () => false,
         getDialect: () => 'impala'
       }
     });
-
+    await setup.waitForKoUpdate();
     expect(element.innerHTML).toMatchSnapshot();
   });
 });

+ 4 - 4
desktop/core/src/desktop/js/ko/components/ko.globalSearch.js

@@ -325,16 +325,16 @@ class GlobalSearch {
     });
   }
 
-  initializeFacetValues() {
-    const self = this;
-    dataCatalog.getAllNavigatorTags({ silenceErrors: true }).done(facets => {
+  async initializeFacetValues() {
+    try {
+      const facets = await dataCatalog.getAllNavigatorTags({ silenceErrors: true });
       const facetValues = self.knownFacetValues();
       facetValues['tags'] = facets;
       facetValues['tag'] = facets;
       if (window.HAS_READ_ONLY_CATALOG) {
         facetValues['classification'] = facets;
       }
-    });
+    } catch (err) {}
   }
 
   cancelRunningRequests() {

+ 1 - 1
desktop/core/src/desktop/js/ko/components/ko.historyPanel.js

@@ -281,7 +281,7 @@ class HistoryPanel {
                       connector: snippet.connector(),
                       path: []
                     })
-                    .done(entry => {
+                    .then(entry => {
                       entry.clearCache({ cascade: true, silenceErrors: true });
                     });
                 } else if (notebook.onSuccessUrl()) {

+ 6 - 6
desktop/core/src/desktop/js/ko/components/ko.navProperties.js

@@ -226,7 +226,7 @@ class NavProperties {
       self.loading(true);
       self.catalogEntry
         .getNavigatorMeta()
-        .done(navigatorMeta => {
+        .then(navigatorMeta => {
           const keysAfterEdit = {};
           const modifiedCustomMetadata = {};
 
@@ -250,11 +250,11 @@ class NavProperties {
 
           self.catalogEntry
             .updateNavigatorCustomMetadata(modifiedCustomMetadata, deletedCustomMetadataKeys)
-            .always(() => {
+            .finally(() => {
               self.loadProperties();
             });
         })
-        .fail(() => {
+        .catch(() => {
           self.loadProperties();
         });
     }
@@ -284,7 +284,7 @@ class NavProperties {
 
     ko.unwrap(self.catalogEntry)
       .getNavigatorMeta()
-      .done(navigatorMeta => {
+      .then(navigatorMeta => {
         const newProps = [];
 
         if (navigatorMeta.classifications) {
@@ -312,10 +312,10 @@ class NavProperties {
         }
         self.properties(newProps);
       })
-      .fail(() => {
+      .catch(() => {
         self.hasErrors(true);
       })
-      .always(() => {
+      .finally(() => {
         self.loading(false);
       });
   }

+ 6 - 6
desktop/core/src/desktop/js/ko/components/ko.navProperties.test.js

@@ -15,8 +15,8 @@
 // limitations under the License.
 
 import { koSetup } from 'jest/koTestUtils';
+import { CancellablePromise } from '../../api/cancellablePromise';
 import { NAME } from './ko.navProperties';
-import $ from 'jquery';
 
 describe('ko.navProperties.js', () => {
   const setup = koSetup();
@@ -26,14 +26,14 @@ describe('ko.navProperties.js', () => {
       catalogEntry: {
         isField: () => true,
         isComplex: () => false,
-        getChildren: () => $.Deferred().resolve([]),
-        getSample: () => $.Deferred().reject(),
-        loadNavigatorMetaForChildren: () => $.Deferred().reject(),
-        loadOptimizerPopularityForChildren: () => $.Deferred().reject(),
+        getChildren: () => CancellablePromise.resolve([]),
+        getSample: () => CancellablePromise.reject(),
+        loadNavigatorMetaForChildren: () => CancellablePromise.reject(),
+        loadOptimizerPopularityForChildren: () => CancellablePromise.reject(),
         isTableOrView: () => false,
         getDialect: () => 'impala',
         getNavigatorMeta: () =>
-          $.Deferred().resolve({
+          CancellablePromise.resolve({
             properties: {
               testProp: 'testVal'
             }

+ 11 - 11
desktop/core/src/desktop/js/ko/components/ko.navTags.js

@@ -98,23 +98,23 @@ class NavTags {
     const currentTagsPromise = ko
       .unwrap(self.catalogEntry)
       .getNavigatorMeta()
-      .done(navigatorMeta => {
+      .then(navigatorMeta => {
         self.currentTags((navigatorMeta && navigatorMeta.tags) || []);
       })
-      .fail(() => {
+      .catch(() => {
         self.hasErrors(true);
       });
 
     const allTagsPromise = dataCatalog
       .getAllNavigatorTags({ silenceErrors: true })
-      .done(tagList => {
+      .then(tagList => {
         self.allTags(Object.keys(tagList));
       })
-      .fail(() => {
+      .catch(() => {
         self.allTags([]);
       });
 
-    $.when(currentTagsPromise, allTagsPromise).always(() => {
+    Promise.all([currentTagsPromise, allTagsPromise]).finally(() => {
       self.loading(false);
     });
   }
@@ -148,25 +148,25 @@ class NavTags {
     const addTagsPromise =
       tagsToAdd.length > 0
         ? ko.unwrap(self.catalogEntry).addNavigatorTags(tagsToAdd)
-        : $.Deferred().resolve().promise();
+        : Promise.resolve();
 
     const deleteTagsPromise =
       tagsToRemove.length > 0
         ? ko.unwrap(self.catalogEntry).deleteNavigatorTags(tagsToRemove)
-        : $.Deferred().resolve().promise();
+        : Promise.resolve();
 
-    $.when(addTagsPromise, deleteTagsPromise)
-      .done(() => {
+    Promise.all([addTagsPromise, deleteTagsPromise])
+      .then(() => {
         if (tagsToAdd.length || tagsToRemove.length) {
           dataCatalog.updateAllNavigatorTags(tagsToAdd, tagsToRemove);
           ko.unwrap(self.catalogEntry).save();
         }
         self.loadTags();
       })
-      .fail(() => {
+      .catch(() => {
         self.hasErrors(true);
       })
-      .always(() => {
+      .finally(() => {
         self.loading(false);
       });
   }

+ 8 - 6
desktop/core/src/desktop/js/ko/components/ko.navTags.test.js

@@ -15,8 +15,8 @@
 // limitations under the License.
 
 import { koSetup } from 'jest/koTestUtils';
+import { CancellablePromise } from '../../api/cancellablePromise';
 import { NAME } from './ko.navTags';
-import $ from 'jquery';
 
 import 'ko/bindings/ko.tagEditor';
 
@@ -28,19 +28,21 @@ describe('ko.navTags.js', () => {
       catalogEntry: {
         isField: () => true,
         isComplex: () => false,
-        getChildren: () => $.Deferred().resolve([]),
-        getSample: () => $.Deferred().reject(),
-        loadNavigatorMetaForChildren: () => $.Deferred().reject(),
-        loadOptimizerPopularityForChildren: () => $.Deferred().reject(),
+        getChildren: () => CancellablePromise.resolve([]),
+        getSample: () => CancellablePromise.reject(),
+        loadNavigatorMetaForChildren: () => CancellablePromise.reject(),
+        loadOptimizerPopularityForChildren: () => CancellablePromise.reject(),
         isTableOrView: () => false,
         getDialect: () => 'impala',
         getNavigatorMeta: () =>
-          $.Deferred().resolve({
+          CancellablePromise.resolve({
             tags: ['testTag']
           })
       }
     });
 
+    await setup.waitForKoUpdate();
+
     expect(element.innerHTML).toMatchSnapshot();
   });
 });

+ 3 - 3
desktop/core/src/desktop/js/ko/components/ko.pollingCatalogEntriesList.js

@@ -108,14 +108,14 @@ class PollingCatalogEntriesList {
             refreshCache: self.pollCount > 0,
             cancellable: true
           })
-          .done(sourceMeta => {
+          .then(sourceMeta => {
             if (sourceMeta.notFound) {
               self.pollForSourceMeta();
             } else {
               self.catalogEntryExists(true);
             }
           })
-          .fail(() => {
+          .catch(() => {
             self.pollForSourceMeta();
           });
       }
@@ -145,7 +145,7 @@ class PollingCatalogEntriesList {
         connector: { id: ko.unwrap(self.sourceType) }, // TODO: Use connectors in polling catalog entries list
         path: ko.unwrap(self.path)
       })
-      .done(catalogEntry => {
+      .then(catalogEntry => {
         self.catalogEntry(catalogEntry);
         self.pollForSourceMeta();
       });

+ 4 - 4
desktop/core/src/desktop/js/parse/types.ts

@@ -14,8 +14,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import CancellableJqPromise from 'api/cancellableJqPromise';
-import DataCatalogEntry from 'catalog/dataCatalogEntry';
+import { CancellablePromise } from 'api/cancellablePromise';
+import DataCatalogEntry from 'catalog/DataCatalogEntry';
 import { ParsedSqlStatement } from './sqlStatementsParser';
 
 export interface IdentifierChainEntry {
@@ -67,10 +67,10 @@ export interface IdentifierLocation {
   parentLocation?: ParsedLocation;
   path?: string;
   qualified?: boolean;
-  resolveCatalogEntry: (options?: {
+  resolveCatalogEntry: (options: {
     cancellable?: boolean;
     temporaryOnly?: boolean;
-  }) => CancellableJqPromise<DataCatalogEntry>;
+  }) => CancellablePromise<DataCatalogEntry>;
 }
 
 export interface StatementDetails {

+ 60 - 60
desktop/core/src/desktop/js/sql/autocompleteResults.js

@@ -552,12 +552,12 @@ class AutocompleteResults {
         );
         const sourceMeta = await new Promise((resolve, reject) => {
           this.onCancelFunctions.push(reject);
-          const sourceMetaDeferred = catalogEntry.getSourceMeta({
+          const sourceMetaPromise = catalogEntry.getSourceMeta({
             silenceErrors: true,
             cancellable: true
           });
-          this.cancellablePromises.push(sourceMetaDeferred);
-          sourceMetaDeferred.done(resolve).fail(reject);
+          this.cancellablePromises.push(sourceMetaPromise);
+          sourceMetaPromise.then(resolve).catch(reject);
         });
         if (typeof sourceMeta.type !== 'undefined') {
           return sourceMeta; // sourceMeta.samples is used in handleValues.
@@ -579,15 +579,15 @@ class AutocompleteResults {
             path: [],
             temporaryOnly: this.temporaryOnly
           })
-          .done(resolve)
-          .fail(reject);
+          .then(resolve)
+          .catch(reject);
       });
 
       return await new Promise((resolve, reject) => {
         this.onCancelFunctions.push(reject);
-        const childrenDeferred = entry.getChildren({ silenceErrors: true, cancellable: true });
-        this.cancellablePromises.push(childrenDeferred);
-        childrenDeferred.done(resolve).fail(reject);
+        const childrenPromise = entry.getChildren({ silenceErrors: true, cancellable: true });
+        this.cancellablePromises.push(childrenPromise);
+        childrenPromise.then(resolve).catch(reject);
       });
     } catch (err) {
       return [];
@@ -886,15 +886,15 @@ class AutocompleteResults {
               path: [database],
               temporaryOnly: this.temporaryOnly
             })
-            .done(resolve)
-            .fail(reject);
+            .then(resolve)
+            .catch(reject);
         });
 
         const tableEntries = await new Promise((resolve, reject) => {
           this.onCancelFunctions.push(reject);
-          const childrenDeferred = dbEntry.getChildren({ silenceErrors: true, cancellable: true });
-          this.cancellablePromises.push(childrenDeferred);
-          childrenDeferred.done(resolve).fail(reject);
+          const childrenPromise = dbEntry.getChildren({ silenceErrors: true, cancellable: true });
+          this.cancellablePromises.push(childrenPromise);
+          childrenPromise.then(resolve).catch(reject);
         });
 
         for (const tableEntry of tableEntries) {
@@ -1125,22 +1125,22 @@ class AutocompleteResults {
       const addColumnsFromEntry = async dataCatalogEntry => {
         const sourceMeta = await new Promise((resolve, reject) => {
           this.onCancelFunctions.push(reject);
-          const sourceMetaDeferred = dataCatalogEntry.getSourceMeta({
+          const sourceMetaPromise = dataCatalogEntry.getSourceMeta({
             silenceErrors: true,
             cancellable: true
           });
-          this.cancellablePromises.push(sourceMetaDeferred);
-          sourceMetaDeferred.done(resolve).fail(reject);
+          this.cancellablePromises.push(sourceMetaPromise);
+          sourceMetaPromise.then(resolve).catch(reject);
         });
 
         const childEntries = await new Promise((resolve, reject) => {
           this.onCancelFunctions.push(reject);
-          const childrenDeferred = dataCatalogEntry.getChildren({
+          const childrenPromise = dataCatalogEntry.getChildren({
             silenceErrors: true,
             cancellable: true
           });
-          this.cancellablePromises.push(childrenDeferred);
-          childrenDeferred.done(resolve).fail(reject);
+          this.cancellablePromises.push(childrenPromise);
+          childrenPromise.then(resolve).catch(reject);
         });
 
         for (const childEntry of childEntries) {
@@ -1482,19 +1482,19 @@ class AutocompleteResults {
             connector: this.snippet.connector(),
             paths: paths
           })
-          .done(resolve)
-          .fail(reject);
+          .then(resolve)
+          .catch(reject);
       });
 
       const topJoins = await new Promise((resolve, reject) => {
         this.onCancelFunctions.push(reject);
-        const topJoinsDeferred = multiTableEntry.getTopJoins({
+        const topJoinsPromise = multiTableEntry.getTopJoins({
           silenceErrors: true,
           cancellable: true,
           connector: this.snippet.connector()
         });
-        this.cancellablePromises.push(topJoinsDeferred);
-        topJoinsDeferred.done(resolve).fail(reject);
+        this.cancellablePromises.push(topJoinsPromise);
+        topJoinsPromise.then(resolve).catch(reject);
       });
 
       let totalCount = 0;
@@ -1607,19 +1607,19 @@ class AutocompleteResults {
             connector: this.snippet.connector(),
             paths: paths
           })
-          .done(resolve)
-          .fail(reject);
+          .then(resolve)
+          .catch(reject);
       });
 
       const topJoins = await new Promise((resolve, reject) => {
         this.onCancelFunctions.push(reject);
-        const topJoinsDeferred = multiTableEntry.getTopJoins({
+        const topJoinsPromise = multiTableEntry.getTopJoins({
           silenceErrors: true,
           cancellable: true,
           connector: this.snippet.connector()
         });
-        this.cancellablePromises.push(topJoinsDeferred);
-        topJoinsDeferred.done(resolve).fail(reject);
+        this.cancellablePromises.push(topJoinsPromise);
+        topJoinsPromise.then(resolve).catch(reject);
       });
 
       let totalCount = 0;
@@ -1701,19 +1701,19 @@ class AutocompleteResults {
             connector: this.snippet.connector(),
             paths: paths
           })
-          .done(resolve)
-          .fail(reject);
+          .then(resolve)
+          .catch(reject);
       });
 
       const topAggs = await new Promise((resolve, reject) => {
         this.onCancelFunctions.push(reject);
-        const topAggsDeferred = multiTableEntry.getTopAggs({
+        const topAggsPromise = multiTableEntry.getTopAggs({
           silenceErrors: true,
           cancellable: true,
           connector: this.snippet.connector()
         });
-        this.cancellablePromises.push(topAggsDeferred);
-        topAggsDeferred.done(resolve).fail(reject);
+        this.cancellablePromises.push(topAggsPromise);
+        topAggsPromise.then(resolve).catch(reject);
       });
 
       if (!topAggs.values || !topAggs.values.length) {
@@ -1819,7 +1819,7 @@ class AutocompleteResults {
     try {
       const entries = await new Promise((resolve, reject) => {
         this.onCancelFunctions.push(reject);
-        const popularityDeferred = dataCatalog
+        const popularityPromise = dataCatalog
           .getCatalog(this.snippet.connector())
           .loadOptimizerPopularityForTables({
             namespace: this.snippet.namespace(),
@@ -1828,8 +1828,8 @@ class AutocompleteResults {
             silenceErrors: true,
             cancellable: true
           });
-        this.cancellablePromises.push(popularityDeferred);
-        popularityDeferred.done(resolve).fail(reject);
+        this.cancellablePromises.push(popularityPromise);
+        popularityPromise.then(resolve).catch(reject);
       });
 
       let totalColumnCount = 0;
@@ -1942,19 +1942,19 @@ class AutocompleteResults {
             connector: this.snippet.connector(),
             paths: paths
           })
-          .done(resolve)
-          .fail(reject);
+          .then(resolve)
+          .catch(reject);
       });
 
       const topFilters = await new Promise((resolve, reject) => {
         this.onCancelFunctions.push(reject);
-        const topFiltersDeferred = multiTableEntry.getTopFilters({
+        const topFiltersPromise = multiTableEntry.getTopFilters({
           silenceErrors: true,
           cancellable: true,
           connector: this.snippet.connector()
         });
-        this.cancellablePromises.push(topFiltersDeferred);
-        topFiltersDeferred.done(resolve).fail(reject);
+        this.cancellablePromises.push(topFiltersPromise);
+        topFiltersPromise.then(resolve).catch(reject);
       });
 
       let totalCount = 0;
@@ -2035,18 +2035,18 @@ class AutocompleteResults {
             path: [db],
             temporaryOnly: this.temporaryOnly
           })
-          .done(resolve)
-          .fail(reject);
+          .then(resolve)
+          .catch(reject);
       });
 
       const childEntries = await new Promise((resolve, reject) => {
         this.onCancelFunctions.push(reject);
-        const popularityDeferred = entry.loadOptimizerPopularityForChildren({
+        const popularityPromise = entry.loadOptimizerPopularityForChildren({
           silenceErrors: true,
           cancellable: true
         });
-        this.cancellablePromises.push(popularityDeferred);
-        popularityDeferred.done(resolve).fail(reject);
+        this.cancellablePromises.push(popularityPromise);
+        popularityPromise.then(resolve).catch(reject);
       });
 
       let totalPopularity = 0;
@@ -2115,7 +2115,7 @@ class AutocompleteResults {
 
       const popularEntries = await new Promise((resolve, reject) => {
         this.onCancelFunctions.push(reject);
-        const popularityDeferred = dataCatalog
+        const popularityPromise = dataCatalog
           .getCatalog(this.snippet.connector())
           .loadOptimizerPopularityForTables({
             namespace: this.snippet.namespace(),
@@ -2124,8 +2124,8 @@ class AutocompleteResults {
             silenceErrors: true,
             cancellable: true
           });
-        this.cancellablePromises.push(popularityDeferred);
-        popularityDeferred.done(resolve).fail(reject);
+        this.cancellablePromises.push(popularityPromise);
+        popularityPromise.then(resolve).catch(reject);
       });
 
       let valueAttribute = '';
@@ -2332,18 +2332,18 @@ class AutocompleteResults {
             path: fetchedPath,
             temporaryOnly: this.temporaryOnly
           })
-          .done(resolve)
-          .fail(reject);
+          .then(resolve)
+          .catch(reject);
       });
 
       const sourceMeta = await new Promise((resolve, reject) => {
         this.onCancelFunctions.push(reject);
-        const sourceMetaDeferred = catalogEntry.getSourceMeta({
+        const sourceMetaPromise = catalogEntry.getSourceMeta({
           silenceErrors: true,
           cancellable: true
         });
-        this.cancellablePromises.push(sourceMetaDeferred);
-        sourceMetaDeferred.done(resolve).fail(reject);
+        this.cancellablePromises.push(sourceMetaPromise);
+        sourceMetaPromise.then(resolve).catch(reject);
       });
 
       if (
@@ -2384,18 +2384,18 @@ class AutocompleteResults {
             path: [],
             temporaryOnly: this.temporaryOnly
           })
-          .done(resolve)
-          .fail(reject);
+          .then(resolve)
+          .catch(reject);
       });
 
       const databaseEntries = await new Promise((resolve, reject) => {
         this.onCancelFunctions.push(reject);
-        const childrenDeferred = catalogEntry.getChildren({
+        const childrenPromise = catalogEntry.getChildren({
           silenceErrors: true,
           cancellable: true
         });
-        this.cancellablePromises.push(childrenDeferred);
-        childrenDeferred.done(resolve).fail(reject);
+        this.cancellablePromises.push(childrenPromise);
+        childrenPromise.then(resolve).catch(reject);
       });
 
       const firstIsDb = databaseEntries.some(dbEntry =>

+ 87 - 83
desktop/core/src/desktop/js/sql/sqlUtils.ts

@@ -14,10 +14,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import DataCatalogEntry from 'catalog/dataCatalogEntry';
-import $ from 'jquery';
+import { Cancellable, CancellablePromise } from 'api/cancellablePromise';
+import { applyCancellable } from 'catalog/catalogUtils';
+import DataCatalogEntry from 'catalog/DataCatalogEntry';
 
-import CancellableJqPromise from 'api/cancellableJqPromise';
 import dataCatalog from 'catalog/dataCatalog';
 import { IdentifierChainEntry, ParsedLocation, ParsedTable } from 'parse/types';
 import { isReserved } from 'sql/reference/sqlReferenceRepository';
@@ -130,40 +130,43 @@ export const resolveCatalogEntry = (options: {
   cancellable?: boolean;
   identifierChain?: IdentifierChainEntry[];
   tables?: ParsedTable[];
-}): CancellableJqPromise<DataCatalogEntry> => {
-  const cancellablePromises: CancellableJqPromise<unknown>[] = [];
-  const deferred = $.Deferred();
-  const promise = new CancellableJqPromise(deferred, undefined, cancellablePromises);
-  dataCatalog.applyCancellable(promise, { cancellable: !!options.cancellable });
+}): CancellablePromise<DataCatalogEntry> => {
+  const promise = new CancellablePromise<DataCatalogEntry>((resolve, reject, onCancel) => {
+    const cancellablePromises: Cancellable[] = [];
 
-  if (!options.identifierChain) {
-    deferred.reject();
-    return promise;
-  }
-
-  const findInTree = (currentEntry: DataCatalogEntry, fieldsToGo: string[]): void => {
-    if (fieldsToGo.length === 0) {
-      deferred.reject();
+    onCancel(() => {
+      cancellablePromises.forEach(cancellable => cancellable.cancel());
+    });
+    if (!options.identifierChain) {
+      reject();
       return;
     }
 
-    let nextField: string;
-    if (currentEntry.getType() === 'map') {
-      nextField = 'value';
-    } else if (currentEntry.getType() === 'array') {
-      nextField = 'item';
-    } else {
-      nextField = fieldsToGo.shift() || '';
-    }
+    const findInTree = (currentEntry: DataCatalogEntry, fieldsToGo: string[]): void => {
+      if (fieldsToGo.length === 0) {
+        reject();
+        return;
+      }
 
-    cancellablePromises.push(
-      currentEntry
-        .getChildren({
-          cancellable: !!options.cancellable,
-          cachedOnly: !!options.cachedOnly,
-          silenceErrors: true
-        })
-        .done(childEntries => {
+      let nextField: string;
+      if (currentEntry.getType() === 'map') {
+        nextField = 'value';
+      } else if (currentEntry.getType() === 'array') {
+        nextField = 'item';
+      } else {
+        nextField = fieldsToGo.shift() || '';
+      }
+
+      const childPromise = currentEntry.getChildren({
+        cancellable: !!options.cancellable,
+        cachedOnly: !!options.cachedOnly,
+        silenceErrors: true
+      });
+
+      cancellablePromises.push(childPromise);
+
+      childPromise
+        .then(childEntries => {
           let foundEntry = undefined;
           childEntries.some((childEntry: { name: string }) => {
             if (identifierEquals(childEntry.name, nextField)) {
@@ -174,40 +177,41 @@ export const resolveCatalogEntry = (options: {
           if (foundEntry && fieldsToGo.length) {
             findInTree(foundEntry, fieldsToGo);
           } else if (foundEntry) {
-            deferred.resolve(foundEntry);
+            resolve(foundEntry);
           } else {
-            deferred.reject();
+            reject();
           }
         })
-        .fail(deferred.reject)
-    );
-  };
+        .catch(reject);
+    };
 
-  const findTable = (tablesToGo: ParsedTable[]): void => {
-    if (tablesToGo.length === 0) {
-      deferred.reject();
-      return;
-    }
+    const findTable = (tablesToGo: ParsedTable[]): void => {
+      if (tablesToGo.length === 0) {
+        reject();
+        return;
+      }
 
-    const nextTable = tablesToGo.pop();
-    if (nextTable && typeof nextTable.subQuery !== 'undefined') {
-      findTable(tablesToGo);
-      return;
-    }
+      const nextTable = tablesToGo.pop();
+      if (nextTable && typeof nextTable.subQuery !== 'undefined') {
+        findTable(tablesToGo);
+        return;
+      }
 
-    cancellablePromises.push(
-      dataCatalog
-        .getChildren({
-          connector: options.connector,
-          namespace: options.namespace,
-          compute: options.compute,
-          path: identifierChainToPath((nextTable && nextTable.identifierChain) || []),
-          cachedOnly: !!options.cachedOnly,
-          cancellable: !!options.cancellable,
-          temporaryOnly: !!options.temporaryOnly,
-          silenceErrors: true
-        })
-        .done(childEntries => {
+      const childPromise = dataCatalog.getChildren({
+        connector: options.connector,
+        namespace: options.namespace,
+        compute: options.compute,
+        path: identifierChainToPath((nextTable && nextTable.identifierChain) || []),
+        cachedOnly: !!options.cachedOnly,
+        cancellable: !!options.cancellable,
+        temporaryOnly: !!options.temporaryOnly,
+        silenceErrors: true
+      });
+
+      cancellablePromises.push(childPromise);
+
+      childPromise
+        .then(childEntries => {
           let foundEntry = undefined;
           childEntries.some((childEntry: { name: string }) => {
             if (
@@ -223,35 +227,35 @@ export const resolveCatalogEntry = (options: {
           if (foundEntry && options.identifierChain && options.identifierChain.length > 1) {
             findInTree(foundEntry, identifierChainToPath(options.identifierChain.slice(1)));
           } else if (foundEntry) {
-            deferred.resolve(foundEntry);
+            resolve(foundEntry);
           } else {
             findTable(tablesToGo);
           }
         })
-        .fail(deferred.reject)
-    );
-  };
+        .catch(reject);
+    };
 
-  if (options.tables) {
-    findTable(options.tables.concat());
-  } else {
-    dataCatalog
-      .getEntry({
-        namespace: options.namespace,
-        compute: options.compute,
-        connector: options.connector,
-        path: [],
-        cachedOnly: !!options.cachedOnly,
-        temporaryOnly: !!options.temporaryOnly
-      })
-      .done(entry => {
-        if (options.identifierChain) {
-          findInTree(entry, identifierChainToPath(options.identifierChain));
-        }
-      });
-  }
+    if (options.tables) {
+      findTable(options.tables.concat());
+    } else {
+      dataCatalog
+        .getEntry({
+          namespace: options.namespace,
+          compute: options.compute,
+          connector: options.connector,
+          path: [],
+          cachedOnly: !!options.cachedOnly,
+          temporaryOnly: !!options.temporaryOnly
+        })
+        .then(entry => {
+          if (options.identifierChain) {
+            findInTree(entry, identifierChainToPath(options.identifierChain));
+          }
+        });
+    }
+  });
 
-  return promise;
+  return applyCancellable(promise, options);
 };
 
 export default {

+ 2 - 2
desktop/core/src/desktop/js/sql/sqlWorkerHandler.js

@@ -16,9 +16,9 @@
 
 import $ from 'jquery';
 
-import dataCatalog from 'catalog/dataCatalog';
 import huePubSub from 'utils/huePubSub';
 import { resolveCatalogEntry } from 'sql/sqlUtils';
+import { applyCancellable } from '../catalog/catalogUtils';
 
 export const POST_TO_LOCATION_WORKER_EVENT = 'ace.sql.location.worker.post';
 export const POST_FROM_LOCATION_WORKER_EVENT = 'ace.sql.location.worker.message';
@@ -31,7 +31,7 @@ const attachEntryResolver = function (location, connector, namespace, compute) {
       options = {};
     }
     if (location.resolvePathPromise && !location.resolvePathPromise.cancelled) {
-      dataCatalog.applyCancellable(location.resolvePathPromise, options);
+      applyCancellable(location.resolvePathPromise, options);
       return location.resolvePathPromise;
     }
 

+ 8 - 8
desktop/core/src/desktop/static/desktop/js/jquery.hiveautocomplete.js

@@ -167,11 +167,11 @@
         validateTimeout = window.setTimeout(function () {
           $.when(self.namespaceDeferred, self.computeDeferred).done(function (namespace, compute) {
             var target = path.pop();
-            dataCatalog.getChildren({ connector: connector, namespace: namespace, compute: compute, path: path }).done(function (childEntries) {
+            dataCatalog.getChildren({ connector: connector, namespace: namespace, compute: compute, path: path }).then(function (childEntries) {
               if (childEntries.some(function (childEntry) { return childEntry.name === target })) {
                 onPathChange($el.val());
               }
-            });
+            }).catch(function() {});
           });
         }, 500);
       }
@@ -273,9 +273,9 @@
     self.getDatabases = function (callback) {
       var self = this;
       $.when(self.namespaceDeferred, self.computeDeferred).done(function (namespace, compute) {
-        dataCatalog.getChildren({ connector: { id: self.options.apiHelperType }, namespace: namespace, compute: compute, path: [] }).done(function (dbEntries) {
+        dataCatalog.getChildren({ connector: { id: self.options.apiHelperType }, namespace: namespace, compute: compute, path: [] }).then(function (dbEntries) {
           callback($.map(dbEntries, function (entry) { return entry.name }));
-        });
+        }).catch(function() {});
       })
     };
 
@@ -287,8 +287,8 @@
     self.getTables = function (database, callback) {
       var self = this;
       $.when(self.namespaceDeferred, self.computeDeferred).done(function (namespace, compute) {
-        dataCatalog.getEntry({ connector: connector, namespace: namespace, compute: compute, path: [ database ] }).done(function (entry) {
-          entry.getSourceMeta().done(callback)
+        dataCatalog.getEntry({ connector: connector, namespace: namespace, compute: compute, path: [ database ] }).then(function (entry) {
+          entry.getSourceMeta().then(callback)
         });
       });
     };
@@ -296,8 +296,8 @@
     self.getColumns = function (database, table, callback) {
       var self = this;
       $.when(self.namespaceDeferred, self.computeDeferred).done(function (namespace, compute) {
-        dataCatalog.getEntry({ connector: connector, namespace: namespace, compute: compute, path: [ database, table ] }).done(function (entry) {
-          entry.getSourceMeta().done(callback)
+        dataCatalog.getEntry({ connector: connector, namespace: namespace, compute: compute, path: [ database, table ] }).then(function (entry) {
+          entry.getSourceMeta().then(callback)
         });
       });
     };

+ 4 - 2
desktop/core/src/desktop/templates/hue_ace_autocompleter.mako

@@ -738,9 +738,11 @@ from desktop.views import _ko
         } else {
           self.loading(true);
           self.loadTimeout = window.setTimeout(function () {
-            self.activePromises.push(self.catalogEntry.getComment({ silenceErrors: true, cancellable: true }).done(self.comment).always(function () {
+            var commentPromise = self.catalogEntry.getComment({ silenceErrors: true, cancellable: true });
+            self.activePromises.push(commentPromise);
+            commentPromise.then(self.comment).finally(function () {
               self.loading(false);
-            }));
+            });
           }, COMMENT_LOAD_DELAY);
         }
 

+ 7 - 7
desktop/libs/indexer/src/indexer/templates/importer.mako

@@ -2178,11 +2178,11 @@ ${ commonheader(_("Importer"), "indexer", user, request, "60px") | n,unicode }
               connector: { id: self.sourceType }, // TODO: Use connectors in the importer
               namespace: wizard.namespace(),
               path: self.outputFormat() === 'table' ? [self.databaseName(), self.tableName()] : [],
-            }).done(function (catalogEntry) {
-              catalogEntry.getSourceMeta({ silenceErrors: true }).done(function (sourceMeta) {
+            }).then(function (catalogEntry) {
+              catalogEntry.getSourceMeta({ silenceErrors: true }).then(function (sourceMeta) {
                 self.isTargetExisting(self.outputFormat() === 'table' ? !sourceMeta.notFound : (sourceMeta.databases || []).indexOf(self.databaseName()) >= 0);
                 self.isTargetChecking(false);
-              }).fail(function () {
+              }).catch(function () {
                 self.isTargetExisting(false);
                 self.isTargetChecking(false);
               })
@@ -2914,14 +2914,14 @@ ${ commonheader(_("Importer"), "indexer", user, request, "60px") | n,unicode }
                       var match = snippet.statement_raw().match(/CREATE TABLE `([^`]+)`/i);
                       if (match) {
                         var db = match[1];
-                        dataCatalog.getEntry({ connector: snippet.connector(), namespace: self.namespace(), compute: self.compute(), path: [ db ]}).done(function (dbEntry) {
-                          dbEntry.clearCache({ silenceErrors: true }).done(function () {
+                        dataCatalog.getEntry({ connector: snippet.connector(), namespace: self.namespace(), compute: self.compute(), path: [ db ]}).then(function (dbEntry) {
+                          dbEntry.clearCache({ silenceErrors: true }).then(function () {
                             huePubSub.publish('open.link', self.editorVM.selectedNotebook().onSuccessUrl());
                           })
                         });
                       } else {
-                        dataCatalog.getEntry({ connector: snippet.connector(), namespace: self.namespace(), compute: self.compute(), path: []}).done(function (sourceEntry) {
-                          sourceEntry.clearCache({ silenceErrors: true }).done(function () {
+                        dataCatalog.getEntry({ connector: snippet.connector(), namespace: self.namespace(), compute: self.compute(), path: []}).then(function (sourceEntry) {
+                          sourceEntry.clearCache({ silenceErrors: true }).then(function () {
                             huePubSub.publish('open.link', self.editorVM.selectedNotebook().onSuccessUrl());
                           })
                         });