Pārlūkot izejas kodu

[catalog] Clean up and move Optimizer API strategy to Axios

This also introduces types for all Optimizer related functions.
Johan Ahlen 5 gadi atpakaļ
vecāks
revīzija
9d8fa93ba6

+ 6 - 4
desktop/core/src/desktop/js/apps/notebook2/snippet.js

@@ -792,7 +792,8 @@ export default class Snippet {
               }
               handleRiskResponse(data);
             })
-            .always(() => {
+            .catch(() => {})
+            .finally(() => {
               changeSubscription.dispose();
             });
         }
@@ -918,7 +919,8 @@ export default class Snippet {
         } else {
           $(document).trigger('error', data.message);
         }
-      });
+      })
+      .catch(() => {});
   }
 
   handleAjaxError(data, callback) {
@@ -1077,12 +1079,12 @@ export default class Snippet {
           $(document).trigger('error', data.message);
         }
       })
-      .fail(xhr => {
+      .catch(xhr => {
         if (xhr.status !== 502) {
           $(document).trigger('error', xhr.responseText);
         }
       })
-      .always(() => {
+      .finally(() => {
         this.compatibilityCheckRunning(false);
       });
   }

+ 183 - 159
desktop/core/src/desktop/js/catalog/DataCatalogEntry.ts

@@ -27,13 +27,7 @@ import { getOptimizer } from './optimizer/optimizer';
 import * as ko from 'knockout';
 
 import apiHelper from 'api/apiHelper';
-import CancellableJqPromise from 'api/cancellableJqPromise';
-import {
-  applyCancellable,
-  fetchAndSave,
-  FetchOptions,
-  forceSilencedErrors
-} from 'catalog/catalogUtils';
+import { applyCancellable, forceSilencedErrors } from 'catalog/catalogUtils';
 import { Compute, Connector, Namespace } from 'types/config';
 import { hueWindow } from 'types/types';
 import huePubSub from 'utils/huePubSub';
@@ -65,6 +59,10 @@ export interface KeySpecification {
   name: string;
 }
 
+export interface ForeignKeySpecification extends KeySpecification {
+  to: string;
+}
+
 export interface ExtendedColumn extends BaseDefinition, TimestampedData {
   name: string;
   type: string;
@@ -93,7 +91,7 @@ export interface TableSourceMeta extends TimestampedData {
   columns: string[];
   comment?: string | null;
   extended_columns: ExtendedColumn[];
-  foreign_keys?: KeySpecification[];
+  foreign_keys?: ForeignKeySpecification[];
   fields?: string[]; // TODO: On FieldSourceMeta?
   hdfs_link?: string;
   is_view?: boolean;
@@ -229,43 +227,6 @@ const cachedOnly = (options?: CatalogGetOptions): boolean => !!(options && optio
 const shouldReload = (options?: CatalogGetOptions & { refreshAnalysis?: boolean }): boolean =>
   !!(!DataCatalog.cacheEnabled() || (options && (options.refreshCache || options.refreshAnalysis)));
 
-/**
- * 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
  */
@@ -375,6 +336,9 @@ export default class DataCatalogEntry {
             parent.navigatorMetaForChildrenPromise = undefined;
             parent.optimizerPopularityForChildrenPromise = undefined;
           }
+        })
+        .catch(err => {
+          console.warn(err);
         });
     }
   }
@@ -382,7 +346,7 @@ export default class DataCatalogEntry {
   /**
    * Resets the entry and clears the cache
    */
-  async clearCache(options: {
+  async clearCache(options?: {
     cascade?: boolean;
     silenceErrors?: boolean;
     targetChild?: string;
@@ -422,7 +386,6 @@ export default class DataCatalogEntry {
 
       onCancel(() => {
         fetchPromise.cancel();
-        this.analysisPromise = undefined;
       });
 
       try {
@@ -439,58 +402,75 @@ export default class DataCatalogEntry {
 
   private reloadNavigatorMeta(options?: ReloadOptions): CancellablePromise<NavigatorMeta> {
     if (this.canHaveNavigatorMetadata()) {
-      this.navigatorMetaPromise = new CancellablePromise<NavigatorMeta>(
+      this.navigatorMetaPromise = new CancellablePromise<NavigatorMeta>(async (resolve, reject) => {
+        try {
+          this.navigatorMeta = await fetchNavigatorMetadata({ ...options, entry: this });
+          resolve(this.navigatorMeta);
+        } catch (err) {
+          reject(err || 'Fetch failed');
+          return;
+        }
+        this.saveLater();
+        if (this.commentObservable) {
+          this.commentObservable(this.getResolvedComment());
+        }
+      });
+    } else {
+      this.navigatorMetaPromise = CancellablePromise.reject();
+    }
+    return applyCancellable(this.navigatorMetaPromise);
+  }
+
+  /**
+   * Helper function to reload the nav opt metadata for the given entry
+   */
+  private reloadOptimizerMeta(options?: ReloadOptions): CancellablePromise<OptimizerMeta> {
+    const optimizer = getOptimizer(this.getConnector());
+    if (this.dataCatalog.canHaveOptimizerMeta()) {
+      this.optimizerMetaPromise = new CancellablePromise<OptimizerMeta>(
         async (resolve, reject, onCancel) => {
+          const fetchPromise = optimizer.fetchOptimizerMeta({
+            path: this.path,
+            silenceErrors: options && options.silenceErrors
+          });
           onCancel(() => {
-            this.navigatorMetaPromise = undefined;
+            fetchPromise.cancel();
           });
+
           try {
-            this.navigatorMeta = await fetchNavigatorMetadata({ ...options, entry: this });
-            resolve(this.navigatorMeta);
+            this.optimizerMeta = await fetchPromise;
+            resolve(this.optimizerMeta);
           } catch (err) {
             reject(err || 'Fetch failed');
             return;
           }
           this.saveLater();
-          if (this.commentObservable) {
-            this.commentObservable(this.getResolvedComment());
-          }
         }
       );
     } else {
-      this.navigatorMetaPromise = CancellablePromise.reject();
+      this.optimizerMetaPromise = CancellablePromise.reject();
     }
-    return applyCancellable(this.navigatorMetaPromise);
+    return applyCancellable(this.optimizerMetaPromise, options);
   }
 
   private reloadPartitions(options?: ReloadOptions): CancellablePromise<Partitions> {
-    this.partitionsPromise = new CancellablePromise<Partitions>(
-      async (resolve, reject, onCancel) => {
-        onCancel(() => {
-          this.partitionsPromise = undefined;
-        });
-
-        try {
-          this.partitions = await fetchPartitions({ ...options, entry: this });
-          resolve(this.partitions);
-        } catch (err) {
-          reject(err || 'Fetch failed');
-          return;
-        }
-        this.saveLater();
+    this.partitionsPromise = new CancellablePromise<Partitions>(async (resolve, reject) => {
+      try {
+        this.partitions = await fetchPartitions({ ...options, entry: this });
+        resolve(this.partitions);
+      } catch (err) {
+        reject(err || 'Fetch failed');
+        return;
       }
-    );
+      this.saveLater();
+    });
     return applyCancellable(this.partitionsPromise, options);
   }
 
   private reloadSample(
     options?: ReloadOptions & { operation?: string }
   ): CancellablePromise<Sample> {
-    this.samplePromise = new CancellablePromise<Sample>(async (resolve, reject, onCancel) => {
-      onCancel(() => {
-        this.samplePromise = undefined;
-      });
-
+    this.samplePromise = new CancellablePromise<Sample>(async (resolve, reject) => {
       try {
         this.sample = await fetchSample({ ...options, entry: this });
         resolve(this.sample);
@@ -504,31 +484,25 @@ export default class DataCatalogEntry {
   }
 
   private reloadSourceMeta(options?: ReloadOptions): CancellablePromise<SourceMeta> {
-    this.sourceMetaPromise = new CancellablePromise<SourceMeta>(
-      async (resolve, reject, onCancel) => {
-        onCancel(() => {
-          this.sourceMetaPromise = undefined;
-        });
-
-        if (this.dataCatalog.invalidatePromise) {
-          try {
-            await this.dataCatalog.invalidatePromise;
-          } catch (err) {}
-        }
-
+    this.sourceMetaPromise = new CancellablePromise<SourceMeta>(async (resolve, reject) => {
+      if (this.dataCatalog.invalidatePromise) {
         try {
-          this.sourceMeta = await fetchSourceMetadata({
-            ...options,
-            entry: this
-          });
-          resolve(this.sourceMeta);
-        } catch (err) {
-          reject(err || 'Fetch failed');
-          return;
-        }
-        this.saveLater();
+          await this.dataCatalog.invalidatePromise;
+        } catch (err) {}
       }
-    );
+
+      try {
+        this.sourceMeta = await fetchSourceMetadata({
+          ...options,
+          entry: this
+        });
+        resolve(this.sourceMeta);
+      } catch (err) {
+        reject(err || 'Fetch failed');
+        return;
+      }
+      this.saveLater();
+    });
     return applyCancellable(this.sourceMetaPromise, options);
   }
 
@@ -573,6 +547,9 @@ export default class DataCatalogEntry {
    * Get the children of the catalog entry, columns for a table entry etc.
    */
   getChildren(options?: CatalogGetOptions): CancellablePromise<DataCatalogEntry[]> {
+    if (this.childrenPromise && this.childrenPromise.cancelled) {
+      this.childrenPromise = undefined;
+    }
     if (!this.childrenPromise && cachedOnly(options)) {
       return CancellablePromise.reject();
     }
@@ -584,19 +561,29 @@ export default class DataCatalogEntry {
     this.childrenPromise = new CancellablePromise<DataCatalogEntry[]>(
       async (resolve, reject, onCancel) => {
         let sourceMeta: SourceMeta | undefined;
+        let cancelled = false;
+        onCancel(() => {
+          cancelled = true;
+        });
         try {
-          const sourceMetaPromise = this.getSourceMeta(options);
-          onCancel(() => {
-            sourceMetaPromise.cancel();
-          });
-          sourceMeta = await sourceMetaPromise;
+          sourceMeta = await this.getSourceMeta(options);
         } catch (err) {}
 
-        if (!sourceMeta || sourceMeta.notFound) {
+        if (cancelled) {
+          reject('Cancelled');
+          return;
+        }
+
+        if (!sourceMeta) {
           reject('No source meta found');
           return;
         }
 
+        if (sourceMeta.notFound) {
+          resolve([]);
+          return;
+        }
+
         const partitionKeys: { [key: string]: boolean } = {};
         const tableSourceMeta = <TableSourceMeta>sourceMeta;
         if (tableSourceMeta.partition_keys) {
@@ -636,37 +623,41 @@ export default class DataCatalogEntry {
               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';
+            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];
+                  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();
                 }
-                definition.index = index++;
-                catalogEntry.definition = definition;
-                catalogEntry.saveLater();
-              }
-            });
+              })
+              .catch(err => {
+                console.warn(err);
+              });
             promises.push(promise);
           }
         });
@@ -688,17 +679,21 @@ export default class DataCatalogEntry {
                 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();
-                }
-              });
+              promise
+                .then(catalogEntry => {
+                  if (
+                    !catalogEntry.definition ||
+                    typeof catalogEntry.definition.index === 'undefined'
+                  ) {
+                    definition.index = index++;
+                    definition.isMapValue = path === 'value';
+                    catalogEntry.definition = definition;
+                    catalogEntry.saveLater();
+                  }
+                })
+                .catch(err => {
+                  console.warn(err);
+                });
               promises.push(promise);
             }
           });
@@ -717,6 +712,9 @@ export default class DataCatalogEntry {
   loadNavigatorMetaForChildren(
     options?: Omit<CatalogGetOptions, 'cachedOnly'>
   ): CancellablePromise<DataCatalogEntry[]> {
+    if (this.navigatorMetaForChildrenPromise && this.navigatorMetaForChildrenPromise.cancelled) {
+      this.navigatorMetaPromise = undefined;
+    }
     options = forceSilencedErrors(options);
 
     if (!this.canHaveNavigatorMetadata() || this.isField()) {
@@ -899,6 +897,12 @@ export default class DataCatalogEntry {
   loadOptimizerPopularityForChildren(
     options?: CatalogGetOptions
   ): CancellablePromise<DataCatalogEntry[]> {
+    if (
+      this.optimizerPopularityForChildrenPromise &&
+      this.optimizerPopularityForChildrenPromise.cancelled
+    ) {
+      this.optimizerPopularityForChildrenPromise = undefined;
+    }
     options = forceSilencedErrors(options);
 
     if (!this.dataCatalog.canHaveOptimizerMeta()) {
@@ -926,23 +930,28 @@ export default class DataCatalogEntry {
       );
     } else if (this.isDatabase() || this.isTableOrView()) {
       this.optimizerPopularityForChildrenPromise = new CancellablePromise<DataCatalogEntry[]>(
-        (resolve, reject, onCancel) => {
+        async (resolve, reject, onCancel) => {
           const cancellablePromises: Cancellable[] = [];
           onCancel(() => {
             cancellablePromises.forEach(cancellable => cancellable.cancel());
           });
 
-          const popularityPromise = getOptimizer(this.dataCatalog.connector).fetchPopularity({
+          const optimizer = getOptimizer(this.dataCatalog.connector);
+          const popularityPromise = optimizer.fetchPopularity({
             ...options,
             paths: [this.path]
           });
           cancellablePromises.push(popularityPromise);
 
-          popularityPromise.done((data: OptimizerResponse) => {
-            const applyPromise = this.applyOptimizerResponseToChildren(data, options);
+          try {
+            const optimzerResponse = await popularityPromise;
+            const applyPromise = this.applyOptimizerResponseToChildren(optimzerResponse, options);
             cancellablePromises.push(applyPromise);
-            applyPromise.then(resolve).catch(reject);
-          });
+            const entries = await applyPromise;
+            resolve(entries);
+          } catch (err) {
+            resolve([]);
+          }
         }
       );
     } else {
@@ -1034,7 +1043,6 @@ export default class DataCatalogEntry {
         resolve(this.definition.comment);
       } else {
         const sourceMetaPromise = this.getSourceMeta(options);
-        cancellablePromises.push(sourceMetaPromise);
         try {
           const sourceMeta = await sourceMetaPromise;
           resolve((sourceMeta && sourceMeta.comment) || '');
@@ -1310,7 +1318,7 @@ export default class DataCatalogEntry {
     if (this.isField()) {
       const type = this.getType();
       if (type) {
-        return `${displayName}(${type})`;
+        return `${displayName} (${type})`;
       }
     }
     return displayName;
@@ -1505,6 +1513,9 @@ export default class DataCatalogEntry {
    * 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 (this.sourceMetaPromise && this.sourceMetaPromise.cancelled) {
+      this.sourceMetaPromise = undefined;
+    }
     if (!this.sourceMetaPromise && cachedOnly(options)) {
       return CancellablePromise.reject();
     }
@@ -1522,6 +1533,9 @@ export default class DataCatalogEntry {
       refreshAnalysis?: boolean;
     }
   ): CancellablePromise<Analysis> {
+    if (this.analysisPromise && this.analysisPromise.cancelled) {
+      this.analysisPromise = undefined;
+    }
     if (!this.analysisPromise && cachedOnly(options)) {
       return CancellablePromise.reject();
     }
@@ -1535,6 +1549,9 @@ export default class DataCatalogEntry {
    * 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.partitionsPromise && this.partitionsPromise.cancelled) {
+      this.partitionsPromise = undefined;
+    }
     if (!this.isTableOrView() || (!this.partitionsPromise && cachedOnly(options))) {
       return CancellablePromise.reject();
     }
@@ -1548,6 +1565,9 @@ export default class DataCatalogEntry {
    * 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> {
+    if (this.navigatorMetaPromise && this.navigatorMetaPromise.cancelled) {
+      this.navigatorMetaPromise = undefined;
+    }
     options = forceSilencedErrors(options);
     if (!this.canHaveNavigatorMetadata() || (!this.navigatorMetaPromise && cachedOnly(options))) {
       return CancellablePromise.reject();
@@ -1562,6 +1582,9 @@ export default class DataCatalogEntry {
    * 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> {
+    if (this.optimizerMetaPromise && this.optimizerMetaPromise.cancelled) {
+      this.optimizerMetaPromise = undefined;
+    }
     options = forceSilencedErrors(options);
 
     if (!this.dataCatalog.canHaveOptimizerMeta() || !this.isTableOrView()) {
@@ -1571,7 +1594,7 @@ export default class DataCatalogEntry {
       return CancellablePromise.reject();
     }
     if (!this.optimizerMetaPromise || shouldReload(options)) {
-      return applyCancellable(reloadOptimizerMeta(this, options), options);
+      return this.reloadOptimizerMeta(options);
     }
     return applyCancellable(this.optimizerMetaPromise, options);
   }
@@ -1585,6 +1608,9 @@ export default class DataCatalogEntry {
       operation?: string;
     }
   ): CancellablePromise<Sample> {
+    if (this.samplePromise && this.samplePromise.cancelled) {
+      this.samplePromise = undefined;
+    }
     // This prevents caching of any non-standard sample queries, i.e. DISTINCT etc.
     if (options && options.operation && options.operation !== 'default') {
       const operation = options.operation;
@@ -1597,8 +1623,8 @@ export default class DataCatalogEntry {
     }
 
     // Check if parent has a sample that we can reuse
-    if (!this.samplePromise && this.isColumn()) {
-      const samplePromise = new CancellablePromise<Sample>(async (resolve, reject, onCancel) => {
+    if (!this.samplePromise && this.isColumn() && !shouldReload(options)) {
+      this.samplePromise = new CancellablePromise<Sample>(async (resolve, reject, onCancel) => {
         const cancellablePromises: Cancellable[] = [];
 
         onCancel(() => {
@@ -1627,7 +1653,7 @@ export default class DataCatalogEntry {
             };
             if (parentSample.meta) {
               for (let i = 0; i < parentSample.meta.length; i++) {
-                if (parentSample.meta[i].name.toLowerCase() === self.name.toLowerCase()) {
+                if (parentSample.meta[i].name.toLowerCase() === this.name.toLowerCase()) {
                   colSample.meta[0] = parentSample.meta[i];
                   parentSample.data.forEach(parentRow => {
                     colSample.data.push([parentRow[i]]);
@@ -1648,7 +1674,6 @@ export default class DataCatalogEntry {
           reject();
         } else {
           const reloadPromise = this.reloadSample(options);
-          cancellablePromises.push(reloadPromise);
           try {
             resolve(await reloadPromise);
           } catch (err) {
@@ -1656,8 +1681,7 @@ export default class DataCatalogEntry {
           }
         }
       });
-
-      return applyCancellable(samplePromise, options);
+      return applyCancellable(this.samplePromise, options);
     }
 
     if (!this.samplePromise && cachedOnly(options)) {

+ 29 - 8
desktop/core/src/desktop/js/catalog/MultiTableEntry.ts

@@ -14,11 +14,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import { noop } from 'lodash';
+
+import DataCatalogEntry from 'catalog/DataCatalogEntry';
 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 { applyCancellable } from 'catalog/catalogUtils';
+import { getOptimizer, PopularityOptions } from 'catalog/optimizer/optimizer';
 import { UdfDetails } from 'sql/reference/types';
 import { Connector } from 'types/config';
 import { hueWindow } from 'types/types';
@@ -74,6 +76,25 @@ export interface TopColumns extends TimestampedData {
   values: unknown[];
 }
 
+const fetchAndSave = <T>(
+  optimizerFunction: (option: PopularityOptions) => CancellablePromise<T>,
+  setFunction: (val: T) => void,
+  entry: DataCatalogEntry | MultiTableEntry,
+  apiOptions?: { silenceErrors?: boolean; refreshAnalysis?: boolean }
+): CancellablePromise<T> => {
+  const promise = optimizerFunction({
+    paths: (<MultiTableEntry>entry).paths, // Set for MultiTableEntry
+    silenceErrors: apiOptions && apiOptions.silenceErrors
+  });
+  promise
+    .then(data => {
+      setFunction(data);
+      entry.saveLater();
+    })
+    .catch(noop);
+  return promise;
+};
+
 /**
  * Helper function to reload a Optimizer multi table attribute, like topAggs or topFilters
  */
@@ -82,7 +103,7 @@ const genericOptimizerReload = <T>(
   options: { silenceErrors?: boolean } | undefined,
   promiseSetter: (promise?: CancellablePromise<T>) => void,
   dataAttributeSetter: (val: T) => void,
-  apiHelperFunction: (option: FetchOptions) => CancellableJqPromise<T>
+  optimizerFunction: (option: PopularityOptions) => CancellablePromise<T>
 ): CancellablePromise<T> => {
   const promise = new CancellablePromise<T>((resolve, reject, onCancel) => {
     if (!multiTableEntry.dataCatalog.canHaveOptimizerMeta()) {
@@ -90,7 +111,7 @@ const genericOptimizerReload = <T>(
       return;
     }
     const fetchPromise = fetchAndSave(
-      apiHelperFunction,
+      optimizerFunction,
       dataAttributeSetter,
       multiTableEntry,
       options
@@ -101,11 +122,11 @@ const genericOptimizerReload = <T>(
       }
     });
 
-    fetchPromise.done(resolve).fail(() => {
+    fetchPromise.then(resolve).catch(err => {
       if (fetchPromise.cancelled) {
         promiseSetter(undefined);
       }
-      reject();
+      reject(err);
     });
   });
 
@@ -122,7 +143,7 @@ const genericOptimizerGet = <T>(
   promiseSetter: (promise?: CancellablePromise<T>) => void,
   promiseGetter: () => CancellablePromise<T> | undefined,
   dataAttributeSetter: (val: T) => void,
-  apiHelperFunction: (option: FetchOptions) => CancellableJqPromise<T>
+  apiHelperFunction: (option: PopularityOptions) => CancellablePromise<T>
 ): CancellablePromise<T> => {
   let promise = promiseGetter();
   if (DataCatalog.cacheEnabled() && options && options.cachedOnly) {

+ 0 - 28
desktop/core/src/desktop/js/catalog/catalogUtils.ts

@@ -14,11 +14,8 @@
 // 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 { CatalogGetOptions } from 'catalog/dataCatalog';
-import DataCatalogEntry from 'catalog/DataCatalogEntry';
-import MultiTableEntry from 'catalog/MultiTableEntry';
 import { Compute, Connector } from 'types/config';
 
 export interface FetchOptions {
@@ -31,31 +28,6 @@ export interface FetchOptions {
   isView?: boolean;
 }
 
-/**
- * Wrapper function around ApiHelper calls, it will also save the entry on success.
- */
-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: (<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: (<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
  */

+ 188 - 0
desktop/core/src/desktop/js/catalog/optimizer/ApiStrategy.ts

@@ -0,0 +1,188 @@
+// 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 { CancellablePromise } from 'api/cancellablePromise';
+import { extractErrorMessage, post, successResponseIsError } from 'api/utils';
+import { OptimizerResponse, TimestampedData } from 'catalog/dataCatalog';
+import { OptimizerMeta } from 'catalog/DataCatalogEntry';
+import { TopAggs, TopColumns, TopFilters, TopJoins } from 'catalog/MultiTableEntry';
+import {
+  CompatibilityOptions,
+  MetaOptions,
+  Optimizer,
+  OptimizerRisk,
+  PopularityOptions,
+  RiskOptions,
+  SimilarityOptions
+} from 'catalog/optimizer/optimizer';
+
+/**
+ * Fetches the popularity for various aspects of the given tables
+ */
+const genericOptimizerMultiTableFetch = <T extends TimestampedData>(
+  { silenceErrors, paths }: PopularityOptions,
+  url: string
+): CancellablePromise<T> => {
+  const dbTables = new Set<string>();
+  paths.forEach(path => {
+    dbTables.add(path.join('.'));
+  });
+  const data = {
+    dbTables: JSON.stringify([...dbTables.values()])
+  };
+
+  return post<T>(url, data, {
+    silenceErrors,
+    handleSuccess: (response, resolve, reject) => {
+      if (successResponseIsError(response)) {
+        reject(extractErrorMessage(response));
+      } else {
+        response.hueTimestamp = Date.now();
+        resolve(response);
+      }
+    }
+  });
+};
+
+const COMPATIBILITY_URL = '/notebook/api/optimizer/statement/compatibility';
+const RISK_URL = '/notebook/api/optimizer/statement/risk';
+const SIMILARITY_URL = '/notebook/api/optimizer/statement/similarity';
+const TOP_AGGS_URL = '/metadata/api/optimizer/top_aggs';
+const TOP_COLUMNS_URL = '/metadata/api/optimizer/top_columns';
+const TOP_FILTERS_URL = '/metadata/api/optimizer/top_filters';
+const TOP_JOINS_URL = '/metadata/api/optimizer/top_joins';
+const TOP_TABLES_URL = '/metadata/api/optimizer/top_tables';
+const TABLE_DETAILS_URL = '/metadata/api/optimizer/table_details';
+
+export default class ApiStrategy implements Optimizer {
+  analyzeCompatibility({
+    notebookJson,
+    snippetJson,
+    sourcePlatform,
+    targetPlatform,
+    silenceErrors
+  }: CompatibilityOptions): CancellablePromise<unknown> {
+    return post<unknown>(
+      COMPATIBILITY_URL,
+      {
+        notebook: notebookJson,
+        snippet: snippetJson,
+        sourcePlatform,
+        targetPlatform
+      },
+      { silenceErrors }
+    );
+  }
+
+  analyzeRisk({
+    notebookJson,
+    snippetJson,
+    silenceErrors
+  }: RiskOptions): CancellablePromise<OptimizerRisk> {
+    return post<OptimizerRisk>(
+      RISK_URL,
+      {
+        notebook: notebookJson,
+        snippet: snippetJson
+      },
+      { silenceErrors }
+    );
+  }
+
+  analyzeSimilarity({
+    notebookJson,
+    snippetJson,
+    sourcePlatform,
+    silenceErrors
+  }: SimilarityOptions): CancellablePromise<unknown> {
+    return post<unknown>(
+      SIMILARITY_URL,
+      {
+        notebook: notebookJson,
+        snippet: snippetJson,
+        sourcePlatform
+      },
+      { silenceErrors }
+    );
+  }
+
+  fetchPopularity({
+    paths,
+    silenceErrors
+  }: PopularityOptions): CancellablePromise<OptimizerResponse> {
+    let url, data;
+
+    if (paths.length === 1 && paths[0].length === 1) {
+      url = TOP_TABLES_URL;
+      data = {
+        database: paths[0][0]
+      };
+    } else {
+      url = TOP_COLUMNS_URL;
+      data = {
+        dbTables: JSON.stringify(paths.map(path => path.join('.')))
+      };
+    }
+
+    return post<OptimizerResponse>(url, data, {
+      silenceErrors,
+      handleSuccess: (response, resolve, reject) => {
+        if (successResponseIsError(response)) {
+          reject(extractErrorMessage(response));
+        } else {
+          response.hueTimestamp = Date.now();
+          resolve(response);
+        }
+      }
+    });
+  }
+
+  fetchTopAggs(options: PopularityOptions): CancellablePromise<TopAggs> {
+    return genericOptimizerMultiTableFetch<TopAggs>(options, TOP_AGGS_URL);
+  }
+
+  fetchTopColumns(options: PopularityOptions): CancellablePromise<TopColumns> {
+    return genericOptimizerMultiTableFetch<TopColumns>(options, TOP_COLUMNS_URL);
+  }
+
+  fetchTopFilters(options: PopularityOptions): CancellablePromise<TopFilters> {
+    return genericOptimizerMultiTableFetch<TopFilters>(options, TOP_FILTERS_URL);
+  }
+
+  fetchTopJoins(options: PopularityOptions): CancellablePromise<TopJoins> {
+    return genericOptimizerMultiTableFetch<TopJoins>(options, TOP_JOINS_URL);
+  }
+
+  fetchOptimizerMeta({ path, silenceErrors }: MetaOptions): CancellablePromise<OptimizerMeta> {
+    return post<OptimizerMeta>(
+      TABLE_DETAILS_URL,
+      {
+        databaseName: path[0],
+        tableName: path[1]
+      },
+      {
+        silenceErrors,
+        handleSuccess: (response: { status: number; details?: OptimizerMeta }, resolve, reject) => {
+          if (response.status === 0 && response.details) {
+            response.details.hueTimestamp = Date.now();
+            resolve(response.details);
+          }
+          reject(extractErrorMessage(response));
+        }
+      }
+    );
+  }
+}

+ 77 - 0
desktop/core/src/desktop/js/catalog/optimizer/BaseStrategy.ts

@@ -0,0 +1,77 @@
+// 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 { CancellablePromise } from 'api/cancellablePromise';
+import { OptimizerMeta } from 'catalog/DataCatalogEntry';
+import { TopAggs, TopColumns, TopFilters, TopJoins } from 'catalog/MultiTableEntry';
+import {
+  CompatibilityOptions,
+  MetaOptions,
+  Optimizer,
+  OptimizerRisk,
+  PopularityOptions,
+  RiskOptions,
+  SimilarityOptions
+} from 'catalog/optimizer/optimizer';
+
+import { OptimizerResponse } from 'catalog/dataCatalog';
+
+export default class BaseStrategy implements Optimizer {
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  analyzeCompatibility(options: CompatibilityOptions): CancellablePromise<unknown> {
+    return CancellablePromise.reject('analyzeCompatibility is not Implemented');
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  analyzeRisk(options: RiskOptions): CancellablePromise<OptimizerRisk> {
+    return CancellablePromise.reject('analyzeRisk is not Implemented');
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  analyzeSimilarity(options: SimilarityOptions): CancellablePromise<unknown> {
+    return CancellablePromise.reject('analyzeSimilarity is not Implemented');
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  fetchOptimizerMeta(options: MetaOptions): CancellablePromise<OptimizerMeta> {
+    return CancellablePromise.reject('fetchOptimizerMeta is not Implemented');
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  fetchPopularity(options: PopularityOptions): CancellablePromise<OptimizerResponse> {
+    return CancellablePromise.reject('analyzeCompatibility is not Implemented');
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  fetchTopAggs(options: PopularityOptions): CancellablePromise<TopAggs> {
+    return CancellablePromise.reject('fetchTopAggs is not Implemented');
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  fetchTopColumns(options: PopularityOptions): CancellablePromise<TopColumns> {
+    return CancellablePromise.reject('fetchTopColumns is not Implemented');
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  fetchTopFilters(options: PopularityOptions): CancellablePromise<TopFilters> {
+    return CancellablePromise.reject('fetchTopFilters is not Implemented');
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  fetchTopJoins(options: PopularityOptions): CancellablePromise<TopJoins> {
+    return CancellablePromise.reject('fetchTopJoins is not Implemented');
+  }
+}

+ 158 - 0
desktop/core/src/desktop/js/catalog/optimizer/LocalStrategy.ts

@@ -0,0 +1,158 @@
+// 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 { CancellablePromise } from 'api/cancellablePromise';
+import contextCatalog from 'catalog/contextCatalog';
+import { OptimizerMeta, TableSourceMeta } from 'catalog/DataCatalogEntry';
+import { TopAggs, TopColumns, TopFilters, TopJoins } from 'catalog/MultiTableEntry';
+import {
+  CompatibilityOptions,
+  MetaOptions,
+  Optimizer,
+  OptimizerRisk,
+  PopularityOptions,
+  RiskOptions,
+  SimilarityOptions
+} from 'catalog/optimizer/optimizer';
+
+import dataCatalog, { OptimizerResponse } from 'catalog/dataCatalog';
+import sqlParserRepository from 'parse/sql/sqlParserRepository';
+import { Connector, Namespace } from 'types/config';
+import I18n from 'utils/i18n';
+
+export default class LocalStrategy implements Optimizer {
+  connector: Connector;
+
+  constructor(connector: Connector) {
+    this.connector = connector;
+  }
+
+  analyzeRisk(options: RiskOptions): CancellablePromise<OptimizerRisk> {
+    const snippet = JSON.parse(options.snippetJson);
+
+    return new CancellablePromise<OptimizerRisk>(async (resolve, reject) => {
+      if (!this.connector.dialect) {
+        reject();
+        return;
+      }
+      const autocompleter = await sqlParserRepository.getAutocompleteParser(this.connector.dialect);
+
+      const sqlParseResult = autocompleter.parseSql(snippet.statement + ' ', '');
+
+      const hasLimit = sqlParseResult.locations.some(
+        location => location.type === 'limitClause' && !location.missing
+      );
+
+      resolve({
+        status: 0,
+        message: '',
+        query_complexity: {
+          hints: !hasLimit
+            ? [
+                {
+                  riskTables: [],
+                  riskAnalysis: I18n('Query has no limit'),
+                  riskId: 22, // To change
+                  risk: 'low',
+                  riskRecommendation: I18n(
+                    'Append a limit clause to reduce the size of the result set'
+                  )
+                }
+              ]
+            : [],
+          noStats: true,
+          noDDL: false
+        }
+      });
+    });
+  }
+
+  fetchTopJoins({ paths, silenceErrors }: PopularityOptions): CancellablePromise<TopJoins> {
+    const path = paths[0].join('.');
+
+    return new CancellablePromise<TopJoins>((resolve, reject, onCancel) => {
+      contextCatalog
+        .getNamespaces({ connector: this.connector, silenceErrors: !silenceErrors })
+        .then(async (result: { namespaces: Namespace[] }) => {
+          if (!result.namespaces.length || !result.namespaces[0].computes.length) {
+            reject('No namespace or compute found');
+            console.warn(result);
+            return;
+          }
+          const entry = await dataCatalog.getEntry({
+            connector: this.connector,
+            path: path,
+            namespace: result.namespaces[0],
+            compute: result.namespaces[0].computes[0]
+          });
+
+          const sourceMetaPromise = entry.getSourceMeta({ silenceErrors });
+
+          onCancel(() => {
+            sourceMetaPromise.cancel();
+          });
+
+          const sourceMeta = await sourceMetaPromise;
+
+          resolve({
+            values: ((<TableSourceMeta>sourceMeta).foreign_keys || []).map(key => ({
+              totalTableCount: 22,
+              totalQueryCount: 3,
+              joinCols: [{ columns: [path + '.' + key.name, key.to] }],
+              tables: [path].concat(key.to.split('.', 2).join('.')),
+              joinType: 'join'
+            }))
+          });
+        })
+        .catch(reject);
+    });
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  analyzeCompatibility(options: CompatibilityOptions): CancellablePromise<unknown> {
+    return CancellablePromise.reject('analyzeCompatibility is not Implemented');
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  analyzeSimilarity(options: SimilarityOptions): CancellablePromise<unknown> {
+    return CancellablePromise.reject('analyzeSimilarity is not Implemented');
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  fetchOptimizerMeta(options: MetaOptions): CancellablePromise<OptimizerMeta> {
+    return CancellablePromise.reject('fetchOptimizerMeta is not Implemented');
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  fetchPopularity(options: PopularityOptions): CancellablePromise<OptimizerResponse> {
+    return CancellablePromise.reject('fetchPopularity is not Implemented');
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  fetchTopAggs(options: PopularityOptions): CancellablePromise<TopAggs> {
+    return CancellablePromise.reject('fetchTopAggs is not Implemented');
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  fetchTopColumns(options: PopularityOptions): CancellablePromise<TopColumns> {
+    return CancellablePromise.reject('fetchTopColumns is not Implemented');
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  fetchTopFilters(options: PopularityOptions): CancellablePromise<TopFilters> {
+    return CancellablePromise.reject('fetchTopFilters is not Implemented');
+  }
+}

+ 0 - 153
desktop/core/src/desktop/js/catalog/optimizer/apiStrategy.js

@@ -1,153 +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 CancellableJqPromise from 'api/cancellableJqPromise';
-import { simplePost } from 'api/apiUtils';
-import { OPTIMIZER_API } from 'api/urls';
-import BaseStrategy from './baseStrategy';
-
-/**
- * Fetches the popularity for various aspects of the given tables
- *
- * @param {OptimizerOptions} options
- * @param {string} url
- * @return {CancellableJqPromise}
- */
-const genericOptimizerMultiTableFetch = (options, url) => {
-  const deferred = $.Deferred();
-
-  const dbTables = {};
-  options.paths.forEach(path => {
-    dbTables[path.join('.')] = true;
-  });
-  const data = {
-    dbTables: ko.mapping.toJSON(Object.keys(dbTables))
-  };
-
-  const request = simplePost(url, data, {
-    silenceErrors: options.silenceErrors,
-    successCallback: data => {
-      data.hueTimestamp = Date.now();
-      deferred.resolve(data);
-    },
-    errorCallback: deferred.reject
-  });
-
-  return new CancellableJqPromise(deferred, request);
-};
-
-export default class ApiStrategy extends BaseStrategy {
-  analyzeCompatibility(options) {
-    return simplePost(OPTIMIZER_API.COMPATIBILITY, {
-      notebook: options.notebookJson,
-      snippet: options.snippetJson,
-      sourcePlatform: options.sourcePlatform,
-      targetPlatform: options.targetPlatform
-    });
-  }
-
-  analyzeRisk(options) {
-    return simplePost(OPTIMIZER_API.RISK, {
-      notebook: options.notebookJson,
-      snippet: options.snippetJson
-    });
-  }
-
-  analyzeSimilarity(options) {
-    return simplePost(OPTIMIZER_API.SIMILARITY, {
-      notebook: options.notebookJson,
-      snippet: options.snippetJson,
-      sourcePlatform: options.sourcePlatform
-    });
-  }
-
-  fetchPopularity(options) {
-    const deferred = $.Deferred();
-    let url, data;
-
-    if (options.paths.length === 1 && options.paths[0].length === 1) {
-      url = OPTIMIZER_API.TOP_TABLES;
-      data = {
-        database: options.paths[0][0]
-      };
-    } else {
-      url = OPTIMIZER_API.TOP_COLUMNS;
-      const dbTables = [];
-      options.paths.forEach(path => {
-        dbTables.push(path.join('.'));
-      });
-      data = {
-        dbTables: ko.mapping.toJSON(dbTables)
-      };
-    }
-
-    const request = simplePost(url, data, {
-      silenceErrors: options.silenceErrors,
-      successCallback: data => {
-        data.hueTimestamp = Date.now();
-        deferred.resolve(data);
-      },
-      errorCallback: deferred.reject
-    });
-
-    return new CancellableJqPromise(deferred, request);
-  }
-
-  fetchTopAggs(options) {
-    return genericOptimizerMultiTableFetch(options, OPTIMIZER_API.TOP_AGGS);
-  }
-
-  fetchTopColumns(options) {
-    return genericOptimizerMultiTableFetch(options, OPTIMIZER_API.TOP_COLUMNS);
-  }
-
-  fetchTopFilters(options) {
-    return genericOptimizerMultiTableFetch(options, OPTIMIZER_API.TOP_FILTERS);
-  }
-
-  fetchTopJoins(options) {
-    return genericOptimizerMultiTableFetch(options, OPTIMIZER_API.TOP_JOINS);
-  }
-
-  fetchOptimizerMeta(options) {
-    const deferred = $.Deferred();
-
-    const request = simplePost(
-      OPTIMIZER_API.TABLE_DETAILS,
-      {
-        databaseName: options.path[0],
-        tableName: options.path[1]
-      },
-      {
-        silenceErrors: options.silenceErrors,
-        successCallback: response => {
-          if (response.status === 0 && response.details) {
-            response.details.hueTimestamp = Date.now();
-            deferred.resolve(response.details);
-          } else {
-            deferred.reject();
-          }
-        },
-        errorCallback: deferred.reject
-      }
-    );
-
-    return new CancellableJqPromise(deferred, request);
-  }
-}

+ 0 - 109
desktop/core/src/desktop/js/catalog/optimizer/baseStrategy.js

@@ -1,109 +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 CancellableJqPromise from 'api/cancellableJqPromise';
-
-export default class BaseStrategy {
-  constructor(connector) {
-    if (!connector) {
-      console.warn('BaseStrategy instantiated without connector.');
-    }
-    this.connector = connector;
-  }
-
-  analyzeRisk(options) {
-    return $.Deferred().reject();
-  }
-
-  analyzeSimilarity(options) {
-    return $.Deferred().reject();
-  }
-
-  analyzeCompatibility(options) {
-    return $.Deferred().reject();
-  }
-
-  /**
-   * @typedef OptimizerOptions
-   * @property {boolean} [options.silenceErrors]
-   * @property {string[][]} options.paths
-   */
-
-  /**
-   * Fetches optimizer popularity for the children of the given path
-   *
-   * @param {OptimizerOptions} options
-   * @return {CancellableJqPromise}
-   */
-  fetchPopularity(options) {
-    return new CancellableJqPromise($.Deferred().reject());
-  }
-
-  /**
-   * Fetches the popular aggregate functions for the given tables
-   *
-   * @param {OptimizerOptions} options
-   * @return {CancellableJqPromise}
-   */
-  fetchTopAggs(options) {
-    return new CancellableJqPromise($.Deferred().reject());
-  }
-
-  /**
-   * Fetches the popular columns for the given tables
-   *
-   * @param {OptimizerOptions} options
-   * @return {CancellableJqPromise}
-   */
-  fetchTopColumns(options) {
-    return new CancellableJqPromise($.Deferred().reject());
-  }
-
-  /**
-   * Fetches the popular filters for the given tables
-   *
-   * @param {OptimizerOptions} options
-   * @return {CancellableJqPromise}
-   */
-  fetchTopFilters(options) {
-    return new CancellableJqPromise($.Deferred().reject());
-  }
-
-  /**
-   * Fetches the popular joins for the given tables
-   *
-   * @param {OptimizerOptions} options
-   * @return {CancellableJqPromise}
-   */
-  fetchTopJoins(options) {
-    return new CancellableJqPromise($.Deferred().reject());
-  }
-
-  /**
-   * Fetches optimizer meta for the given path, only possible for tables atm.
-   *
-   * @param {Object} options
-   * @param {boolean} [options.silenceErrors]
-   * @param {string[]} options.path
-   *
-   * @return {CancellableJqPromise}
-   */
-  fetchOptimizerMeta(options) {
-    return new CancellableJqPromise($.Deferred().reject());
-  }
-}

+ 0 - 98
desktop/core/src/desktop/js/catalog/optimizer/localStrategy.js

@@ -1,98 +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 BaseStrategy from './baseStrategy';
-import dataCatalog from 'catalog/dataCatalog';
-import sqlParserRepository from 'parse/sql/sqlParserRepository';
-import I18n from 'utils/i18n';
-
-export default class LocalStrategy extends BaseStrategy {
-  analyzeRisk(options) {
-    const snippet = JSON.parse(options.snippetJson);
-
-    const deferred = $.Deferred();
-    sqlParserRepository
-      .getAutocompleteParser(this.connector.dialect)
-      .then(sqlAutocompleteParser => {
-        const sqlParseResult = sqlAutocompleteParser.parseSql(snippet.statement + ' ', '');
-
-        const hasLimit = sqlParseResult.locations.some(
-          location => location.type === 'limitClause' && !location.missing
-        );
-
-        deferred.resolve({
-          status: 0,
-          message: '',
-          query_complexity: {
-            hints: !hasLimit
-              ? [
-                  {
-                    riskTables: [],
-                    riskAnalysis: I18n('Query has no limit'),
-                    riskId: 22, // To change
-                    risk: 'low',
-                    riskRecommendation: I18n(
-                      'Append a limit clause to reduce the size of the result set'
-                    )
-                  }
-                ]
-              : [],
-            noStats: true,
-            noDDL: false
-          }
-        });
-      });
-
-    return deferred.promise();
-  }
-
-  fetchTopJoins(options) {
-    const path = options.paths[0].join('.');
-    const deferred = $.Deferred();
-
-    dataCatalog
-      .getEntry({
-        connector: this.connector,
-        path: path,
-        namespace: { id: 'default' }
-      })
-      .then(entry => {
-        entry
-          .getSourceMeta({ silenceErrors: true })
-          .then(() => {
-            if (!entry.sourceMeta) {
-              entry.sourceMeta = { foreign_keys: [] };
-            }
-            const data = {
-              values: entry.sourceMeta.foreign_keys.map(key => ({
-                totalTableCount: 22,
-                totalQueryCount: 3,
-                joinCols: [{ columns: [path + '.' + key.name, key.to] }],
-                tables: [path].concat(key.to.split('.', 2).join('.')),
-                joinType: 'join'
-              }))
-            };
-            deferred.resolve(data);
-          })
-          .catch(deferred.reject);
-      })
-      .catch(deferred.reject);
-
-    return deferred.promise();
-  }
-}

+ 0 - 42
desktop/core/src/desktop/js/catalog/optimizer/optimizer.js

@@ -1,42 +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 ApiStrategy from './apiStrategy';
-import BaseStrategy from './baseStrategy';
-import LocalStrategy from './localStrategy';
-
-const optimizerInstances = {};
-
-export const LOCAL_STRATEGY = 'local';
-export const API_STRATEGY = 'api';
-
-const createOptimizer = connector => {
-  // TODO: Remove window.OPTIMIZER_MODE and hardcoded { optimizer: 'api' } when 'connector.optimizer_mode' works.
-  if (window.OPTIMIZER_MODE === LOCAL_STRATEGY) {
-    return new LocalStrategy(connector);
-  }
-  if (window.OPTIMIZER_MODE === API_STRATEGY) {
-    return new ApiStrategy(connector);
-  }
-  return new BaseStrategy(connector);
-};
-
-export const getOptimizer = connector => {
-  if (!optimizerInstances[connector.id]) {
-    optimizerInstances[connector.id] = createOptimizer(connector);
-  }
-  return optimizerInstances[connector.id];
-};

+ 109 - 0
desktop/core/src/desktop/js/catalog/optimizer/optimizer.ts

@@ -0,0 +1,109 @@
+// 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 ApiStrategy from './ApiStrategy';
+import LocalStrategy from './LocalStrategy';
+import BaseStrategy from './BaseStrategy';
+import { CancellablePromise } from 'api/cancellablePromise';
+import { OptimizerResponse } from 'catalog/dataCatalog';
+import { OptimizerMeta } from 'catalog/DataCatalogEntry';
+import { TopAggs, TopColumns, TopFilters, TopJoins } from 'catalog/MultiTableEntry';
+import { Connector } from 'types/config';
+import { hueWindow } from 'types/types';
+
+export interface CompatibilityOptions {
+  notebookJson: string;
+  snippetJson: string;
+  sourcePlatform: string;
+  targetPlatform: string;
+  silenceErrors?: boolean;
+}
+
+export interface RiskOptions {
+  notebookJson: string;
+  snippetJson: string;
+  silenceErrors?: boolean;
+}
+
+export interface OptimizerRisk {
+  status: number;
+  message: string;
+  query_complexity: {
+    hints: {
+      riskTables: unknown[];
+      riskAnalysis: string;
+      riskId: number;
+      risk: string;
+      riskRecommendation: string;
+    }[];
+    noStats: boolean;
+    noDDL: boolean;
+  };
+}
+
+export interface SimilarityOptions {
+  notebookJson: string;
+  snippetJson: string;
+  sourcePlatform: string;
+  silenceErrors?: boolean;
+}
+
+export interface PopularityOptions {
+  paths: string[][];
+  silenceErrors?: boolean;
+}
+
+export interface MetaOptions {
+  path: string[];
+  silenceErrors?: boolean;
+}
+
+export interface Optimizer {
+  analyzeRisk(options: RiskOptions): CancellablePromise<OptimizerRisk>;
+  analyzeSimilarity(options: SimilarityOptions): CancellablePromise<unknown>;
+  analyzeCompatibility(options: CompatibilityOptions): CancellablePromise<unknown>;
+  fetchPopularity(options: PopularityOptions): CancellablePromise<OptimizerResponse>;
+  fetchTopAggs(options: PopularityOptions): CancellablePromise<TopAggs>;
+  fetchTopColumns(options: PopularityOptions): CancellablePromise<TopColumns>;
+  fetchTopFilters(options: PopularityOptions): CancellablePromise<TopFilters>;
+  fetchTopJoins(options: PopularityOptions): CancellablePromise<TopJoins>;
+  fetchOptimizerMeta(options: MetaOptions): CancellablePromise<OptimizerMeta>;
+}
+
+const optimizerInstances: { [connectorId: string]: Optimizer | undefined } = {};
+
+export const LOCAL_STRATEGY = 'local';
+export const API_STRATEGY = 'api';
+
+const createOptimizer = (connector: Connector): Optimizer => {
+  // TODO: Remove window.OPTIMIZER_MODE and hardcoded { optimizer: 'api' } when 'connector.optimizer_mode' works.
+  if ((<hueWindow>window).OPTIMIZER_MODE === LOCAL_STRATEGY) {
+    return new LocalStrategy(connector);
+  }
+  if ((<hueWindow>window).OPTIMIZER_MODE === API_STRATEGY) {
+    return new ApiStrategy();
+  }
+  return new BaseStrategy();
+};
+
+export const getOptimizer = (connector: Connector): Optimizer => {
+  let optimizer = optimizerInstances[connector.id];
+  if (!optimizer) {
+    optimizer = createOptimizer(connector);
+    optimizerInstances[connector.id] = optimizer;
+  }
+  return optimizer;
+};

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

@@ -212,7 +212,13 @@ class AceLocationHandler {
                                 return true;
                               });
                             }
+                          })
+                          .catch(() => {
+                            // Ignore
                           });
+                      })
+                      .catch(err => {
+                        // Ignore
                       });
                   }
                 }

+ 1 - 0
desktop/core/src/desktop/js/parse/types.ts

@@ -106,6 +106,7 @@ export interface AutocompleteParseResult {
     alias: string;
     columns: ColumnDetails[];
   }[];
+  locations: IdentifierLocation[];
   lowerCase: boolean;
   subQueries: SubQuery[];
   suggestAggregateFunctions?: {