Преглед на файлове

[catalog] Replace string references with actual functions in the data catalog

Using string references for known attributes and functions is brittle and makes it very difficult to identify dependencies across modules
Johan Ahlen преди 5 години
родител
ревизия
5a9b778c06

+ 5 - 11
desktop/core/src/desktop/js/catalog/catalogUtils.js

@@ -14,23 +14,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import apiHelper from 'api/apiHelper';
-
 /**
  * Wrapper function around ApiHelper calls, it will also save the entry on success.
  *
- * @param {string|Function} apiHelperFunction - The name of the ApiHelper function to call
- * @param {string} attributeName - The attribute to set
+ * @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, attributeName, entry, apiOptions) => {
-  const func =
-    typeof apiHelperFunction === 'string'
-      ? apiHelper[apiHelperFunction].bind(apiHelper)
-      : apiHelperFunction;
-  return func({
+const fetchAndSave = (apiHelperFunction, setFunction, entry, apiOptions) => {
+  return apiHelperFunction({
     sourceType: entry.getConnector().id,
     compute: entry.compute,
     path: entry.path, // Set for DataCatalogEntry
@@ -39,7 +33,7 @@ const fetchAndSave = (apiHelperFunction, attributeName, entry, apiOptions) => {
     connector: entry.dataCatalog.connector,
     isView: entry.isView && entry.isView() // MultiTable entries don't have this property
   }).done(data => {
-    entry[attributeName] = data;
+    setFunction(data);
     entry.saveLater();
   });
 };

+ 97 - 64
desktop/core/src/desktop/js/catalog/dataCatalog.js

@@ -59,6 +59,10 @@ const generateEntryCacheId = function (options) {
   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.
  *
@@ -66,31 +70,55 @@ const generateEntryCacheId = function (options) {
  * @param {Object} storeEntry - The cached version
  */
 const mergeEntry = function (dataCatalogEntry, storeEntry) {
-  const mergeAttribute = function (attributeName, ttl, promiseName) {
+  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.version === DATA_CATALOG_VERSION &&
-      storeEntry[attributeName] &&
-      (!storeEntry[attributeName].hueTimestamp ||
-        Date.now() - storeEntry[attributeName].hueTimestamp < ttl)
+      storeEntry.optimizerPopularity &&
+      isFresh(storeEntry.optimizerPopularity, CACHEABLE_TTL.optimizer)
     ) {
-      dataCatalogEntry[attributeName] = storeEntry[attributeName];
-      if (promiseName) {
-        dataCatalogEntry[promiseName] = $.Deferred()
-          .resolve(dataCatalogEntry[attributeName])
-          .promise();
-      }
+      dataCatalogEntry.optimizerPopularity = storeEntry.optimizerPopularity;
+      dataCatalogEntry.optimizerMetaPromise = $.Deferred()
+        .resolve(dataCatalogEntry.optimizerPopularity)
+        .promise();
     }
-  };
-
-  mergeAttribute('definition', CACHEABLE_TTL.default);
-  mergeAttribute('sourceMeta', CACHEABLE_TTL.default, 'sourceMetaPromise');
-  mergeAttribute('analysis', CACHEABLE_TTL.default, 'analysisPromise');
-  mergeAttribute('partitions', CACHEABLE_TTL.default, 'partitionsPromise');
-  mergeAttribute('sample', CACHEABLE_TTL.default, 'samplePromise');
-  mergeAttribute('navigatorMeta', CACHEABLE_TTL.default, 'navigatorMetaPromise');
-  if (dataCatalogEntry.getConnector().optimizer !== LOCAL_STRATEGY) {
-    mergeAttribute('optimizerMeta', CACHEABLE_TTL.optimizer, 'optimizerMetaPromise');
-    mergeAttribute('optimizerPopularity', CACHEABLE_TTL.optimizer);
   }
 };
 
@@ -101,29 +129,36 @@ const mergeEntry = function (dataCatalogEntry, storeEntry) {
  * @param {Object} storeEntry - The cached version
  */
 const mergeMultiTableEntry = function (multiTableCatalogEntry, storeEntry) {
-  if (multiTableCatalogEntry.getConnector().optimizer === LOCAL_STRATEGY) {
+  if (
+    multiTableCatalogEntry.getConnector().optimizer === LOCAL_STRATEGY ||
+    storeEntry.version !== DATA_CATALOG_VERSION
+  ) {
     return;
   }
-  const mergeAttribute = function (attributeName, ttl, promiseName) {
-    if (
-      storeEntry.version === DATA_CATALOG_VERSION &&
-      storeEntry[attributeName] &&
-      (!storeEntry[attributeName].hueTimestamp ||
-        Date.now() - storeEntry[attributeName].hueTimestamp < ttl)
-    ) {
-      multiTableCatalogEntry[attributeName] = storeEntry[attributeName];
-      if (promiseName) {
-        multiTableCatalogEntry[promiseName] = $.Deferred()
-          .resolve(multiTableCatalogEntry[attributeName])
-          .promise();
-      }
-    }
-  };
-
-  mergeAttribute('topAggs', CACHEABLE_TTL.optimizer, 'topAggsPromise');
-  mergeAttribute('topColumns', CACHEABLE_TTL.optimizer, 'topColumnsPromise');
-  mergeAttribute('topFilters', CACHEABLE_TTL.optimizer, 'topFiltersPromise');
-  mergeAttribute('topJoins', CACHEABLE_TTL.optimizer, 'topJoinsPromise');
+  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 {
@@ -368,7 +403,9 @@ export class DataCatalog {
                   .done(entry => {
                     cancellablePromises.push(
                       entry.trackedPromise(
-                        'optimizerPopularityForChildrenPromise',
+                        promise => {
+                          entry.optimizerPopularityForChildrenPromise = promise;
+                        },
                         entry
                           .applyOptimizerResponseToChildren(perTable[path], options)
                           .done(entries => {
@@ -661,17 +698,15 @@ export class DataCatalog {
     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
-          })
-        )
-        .promise();
+      deferred.resolve(
+        new DataCatalogEntry({
+          dataCatalog: self,
+          namespace: options.namespace,
+          compute: options.compute,
+          path: options.path,
+          definition: options.definition
+        })
+      );
     } else {
       self.store
         .getItem(identifier)
@@ -731,15 +766,13 @@ export class DataCatalog {
     self.multiTableEntries[identifier] = deferred.promise();
 
     if (!cacheEnabled) {
-      deferred
-        .resolve(
-          new MultiTableEntry({
-            identifier: identifier,
-            dataCatalog: self,
-            paths: options.paths
-          })
-        )
-        .promise();
+      deferred.resolve(
+        new MultiTableEntry({
+          identifier: identifier,
+          dataCatalog: self,
+          paths: options.paths
+        })
+      );
     } else {
       self.multiTableStore
         .getItem(identifier)

+ 82 - 40
desktop/core/src/desktop/js/catalog/dataCatalogEntry.js

@@ -41,20 +41,35 @@ const reloadSourceMeta = function (dataCatalogEntry, options) {
     dataCatalogEntry.dataCatalog.invalidatePromise.always(() => {
       cancellablePromises.push(
         catalogUtils
-          .fetchAndSave('fetchSourceMetadata', 'sourceMeta', dataCatalogEntry, options)
+          .fetchAndSave(
+            apiHelper.fetchSourceMetadata.bind(apiHelper),
+            val => {
+              dataCatalogEntry.sourceMeta = val;
+            },
+            dataCatalogEntry,
+            options
+          )
           .done(deferred.resolve)
           .fail(deferred.reject)
       );
     });
-    return dataCatalogEntry.trackedPromise(
-      'sourceMetaPromise',
-      new CancellableJqPromise(deferred, undefined, cancellablePromises)
-    );
+    return dataCatalogEntry.trackedPromise(promise => {
+      dataCatalogEntry.sourceMetaPromise = promise;
+    }, new CancellableJqPromise(deferred, undefined, cancellablePromises));
   }
 
   return dataCatalogEntry.trackedPromise(
-    'sourceMetaPromise',
-    catalogUtils.fetchAndSave('fetchSourceMetadata', 'sourceMeta', dataCatalogEntry, options)
+    promise => {
+      dataCatalogEntry.sourceMetaPromise = promise;
+    },
+    catalogUtils.fetchAndSave(
+      apiHelper.fetchSourceMetadata.bind(apiHelper),
+      val => {
+        dataCatalogEntry.sourceMeta = val;
+      },
+      dataCatalogEntry,
+      options
+    )
   );
 };
 
@@ -71,10 +86,14 @@ const reloadNavigatorMeta = function (dataCatalogEntry, apiOptions) {
   if (dataCatalogEntry.canHaveNavigatorMetadata()) {
     return dataCatalogEntry
       .trackedPromise(
-        'navigatorMetaPromise',
+        promise => {
+          dataCatalogEntry.navigatorMetaPromise = promise;
+        },
         catalogUtils.fetchAndSave(
-          'fetchNavigatorMetadata',
-          'navigatorMeta',
+          apiHelper.fetchNavigatorMetadata.bind(apiHelper),
+          val => {
+            dataCatalogEntry.navigatorMeta = val;
+          },
           dataCatalogEntry,
           apiOptions
         )
@@ -101,10 +120,16 @@ const reloadNavigatorMeta = function (dataCatalogEntry, apiOptions) {
  */
 const reloadAnalysis = function (dataCatalogEntry, apiOptions) {
   return dataCatalogEntry.trackedPromise(
-    'analysisPromise',
+    promise => {
+      dataCatalogEntry.analysisPromise = promise;
+    },
     catalogUtils.fetchAndSave(
-      apiOptions && apiOptions.refreshAnalysis ? 'refreshAnalysis' : 'fetchAnalysis',
-      'analysis',
+      apiOptions && apiOptions.refreshAnalysis
+        ? apiHelper.refreshAnalysis.bind(apiHelper)
+        : apiHelper.fetchAnalysis.bind(apiHelper),
+      val => {
+        dataCatalogEntry.analysis = val;
+      },
       dataCatalogEntry,
       apiOptions
     )
@@ -122,8 +147,17 @@ const reloadAnalysis = function (dataCatalogEntry, apiOptions) {
  */
 const reloadPartitions = function (dataCatalogEntry, apiOptions) {
   return dataCatalogEntry.trackedPromise(
-    'partitionsPromise',
-    catalogUtils.fetchAndSave('fetchPartitions', 'partitions', dataCatalogEntry, apiOptions)
+    promise => {
+      dataCatalogEntry.partitionsPromise = promise;
+    },
+    catalogUtils.fetchAndSave(
+      apiHelper.fetchPartitions.bind(apiHelper),
+      val => {
+        dataCatalogEntry.partitions = val;
+      },
+      dataCatalogEntry,
+      apiOptions
+    )
   );
 };
 
@@ -138,8 +172,17 @@ const reloadPartitions = function (dataCatalogEntry, apiOptions) {
  */
 const reloadSample = function (dataCatalogEntry, apiOptions) {
   return dataCatalogEntry.trackedPromise(
-    'samplePromise',
-    catalogUtils.fetchAndSave('fetchSample', 'sample', dataCatalogEntry, apiOptions)
+    promise => {
+      dataCatalogEntry.samplePromise = promise;
+    },
+    catalogUtils.fetchAndSave(
+      apiHelper.fetchSample.bind(apiHelper),
+      val => {
+        dataCatalogEntry.sample = val;
+      },
+      dataCatalogEntry,
+      apiOptions
+    )
   );
 };
 
@@ -156,10 +199,14 @@ const reloadOptimizerMeta = function (dataCatalogEntry, apiOptions) {
   if (dataCatalogEntry.dataCatalog.canHaveOptimizerMeta()) {
     const optimizer = getOptimizer(dataCatalogEntry.dataCatalog.connector);
     return dataCatalogEntry.trackedPromise(
-      'optimizerMetaPromise',
+      promise => {
+        dataCatalogEntry.optimizerMetaPromise = promise;
+      },
       catalogUtils.fetchAndSave(
         optimizer.fetchOptimizerMeta.bind(optimizer),
-        'optimizerMeta',
+        val => {
+          dataCatalogEntry.optimizerMeta = val;
+        },
         dataCatalogEntry,
         apiOptions
       )
@@ -289,15 +336,14 @@ export default class DataCatalogEntry {
   /**
    * Helper function that ensure that cancellable promises are not tracked anymore when cancelled
    *
-   * @param {string} promiseName - The attribute name to use
+   * @param {Function} promiseSetter - The promise attribute to set
    * @param {CancellableJqPromise} cancellableJqPromise
    */
-  trackedPromise(promiseName, cancellableJqPromise) {
-    const self = this;
-    self[promiseName] = cancellableJqPromise;
+  trackedPromise(promiseSetter, cancellableJqPromise) {
+    promiseSetter(cancellableJqPromise);
     return cancellableJqPromise.fail(() => {
       if (cancellableJqPromise.cancelled) {
-        delete self[promiseName];
+        promiseSetter(undefined);
       }
     });
   }
@@ -518,10 +564,9 @@ export default class DataCatalogEntry {
       .fail(deferred.reject);
 
     return catalogUtils.applyCancellable(
-      self.trackedPromise(
-        'childrenPromise',
-        new CancellableJqPromise(deferred, undefined, [sourceMetaPromise])
-      ),
+      self.trackedPromise(promise => {
+        self.childrenPromise = promise;
+      }, new CancellableJqPromise(deferred, undefined, [sourceMetaPromise])),
       options
     );
   }
@@ -636,10 +681,9 @@ export default class DataCatalogEntry {
     );
 
     return catalogUtils.applyCancellable(
-      self.trackedPromise(
-        'navigatorMetaForChildrenPromise',
-        new CancellableJqPromise(deferred, null, cancellablePromises)
-      ),
+      self.trackedPromise(promise => {
+        self.navigatorMetaForChildrenPromise = promise;
+      }, new CancellableJqPromise(deferred, null, cancellablePromises)),
       options
     );
   }
@@ -782,10 +826,9 @@ export default class DataCatalogEntry {
     }
 
     return catalogUtils.applyCancellable(
-      self.trackedPromise(
-        'optimizerPopularityForChildrenPromise',
-        new CancellableJqPromise(deferred, undefined, cancellablePromises)
-      ),
+      self.trackedPromise(promise => {
+        self.optimizerPopularityForChildrenPromise = promise;
+      }, new CancellableJqPromise(deferred, undefined, cancellablePromises)),
       options
     );
   }
@@ -1725,10 +1768,9 @@ export default class DataCatalogEntry {
         .fail(revertToSpecific);
 
       return catalogUtils.applyCancellable(
-        self.trackedPromise(
-          'samplePromise',
-          new CancellableJqPromise(deferred, undefined, cancellablePromises)
-        ),
+        self.trackedPromise(promise => {
+          self.samplePromise = promise;
+        }, new CancellableJqPromise(deferred, undefined, cancellablePromises)),
         options
       );
     }

+ 53 - 31
desktop/core/src/desktop/js/catalog/multiTableEntry.js

@@ -26,26 +26,27 @@ import { DataCatalog } from './dataCatalog';
  * @param {MultiTableEntry} multiTableEntry
  * @param {Object} [options]
  * @param {boolean} [options.silenceErrors] - Default true
- * @param {string} promiseAttribute
- * @param {string} dataAttribute
+ * @param {Function} promiseSetter
+ * @param {Function} dataAttributeSetter
  * @param {Function} apiHelperFunction
  * @return {CancellableJqPromise}
  */
 const genericOptimizerReload = function (
   multiTableEntry,
   options,
-  promiseAttribute,
-  dataAttribute,
+  promiseSetter,
+  dataAttributeSetter,
   apiHelperFunction
 ) {
   if (multiTableEntry.dataCatalog.canHaveOptimizerMeta()) {
     return multiTableEntry.trackedPromise(
-      promiseAttribute,
-      catalogUtils.fetchAndSave(apiHelperFunction, dataAttribute, multiTableEntry, options)
+      promiseSetter,
+      catalogUtils.fetchAndSave(apiHelperFunction, dataAttributeSetter, multiTableEntry, options)
     );
   }
-  multiTableEntry[promiseAttribute] = $.Deferred().reject();
-  return multiTableEntry[promiseAttribute];
+  const promise = $.Deferred().reject();
+  promiseSetter(promise);
+  return promise;
 };
 
 /**
@@ -57,21 +58,23 @@ const genericOptimizerReload = function (
  * @param {boolean} [options.refreshCache] - Default false
  * @param {boolean} [options.cachedOnly] - Default false
  * @param {boolean} [options.cancellable] - Default false
- * @param {string} promiseAttribute
- * @param {string} dataAttribute
+ * @param {Function} promiseSetter
+ * @param {Function} promiseGetter
+ * @param {Function} dataAttributeSetter
  * @param {Function} apiHelperFunction
  * @return {CancellableJqPromise}
  */
 const genericOptimizerGet = function (
   multiTableEntry,
   options,
-  promiseAttribute,
-  dataAttribute,
+  promiseSetter,
+  promiseGetter,
+  dataAttributeSetter,
   apiHelperFunction
 ) {
   if (DataCatalog.cacheEnabled() && options && options.cachedOnly) {
     return (
-      catalogUtils.applyCancellable(multiTableEntry[promiseAttribute], options) ||
+      catalogUtils.applyCancellable(promiseGetter(), options) ||
       $.Deferred().reject(false).promise()
     );
   }
@@ -80,20 +83,20 @@ const genericOptimizerGet = function (
       genericOptimizerReload(
         multiTableEntry,
         options,
-        promiseAttribute,
-        dataAttribute,
+        promiseSetter,
+        dataAttributeSetter,
         apiHelperFunction
       ),
       options
     );
   }
   return catalogUtils.applyCancellable(
-    multiTableEntry[promiseAttribute] ||
+    promiseGetter() ||
       genericOptimizerReload(
         multiTableEntry,
         options,
-        promiseAttribute,
-        dataAttribute,
+        promiseSetter,
+        dataAttributeSetter,
         apiHelperFunction
       ),
     options
@@ -154,15 +157,14 @@ class MultiTableEntry {
   /**
    * Helper function that ensure that cancellable promises are not tracked anymore when cancelled
    *
-   * @param {string} promiseName - The attribute name to use
+   * @param {Function} promiseSetter - The promise attribute to use
    * @param {CancellableJqPromise} cancellableJqPromise
    */
-  trackedPromise(promiseName, cancellableJqPromise) {
-    const self = this;
-    self[promiseName] = cancellableJqPromise;
+  trackedPromise(promiseSetter, cancellableJqPromise) {
+    promiseSetter(cancellableJqPromise);
     return cancellableJqPromise.fail(() => {
       if (cancellableJqPromise.cancelled) {
-        delete self[promiseName];
+        delete promiseSetter(undefined);
       }
     });
   }
@@ -201,8 +203,13 @@ class MultiTableEntry {
     return genericOptimizerGet(
       this,
       options,
-      'topAggsPromise',
-      'topAggs',
+      promise => {
+        this.topAggsPromise = promise;
+      },
+      () => this.topAggsPromise,
+      val => {
+        this.topAggs = val;
+      },
       optimizer.fetchTopAggs.bind(optimizer)
     );
   }
@@ -223,8 +230,13 @@ class MultiTableEntry {
     return genericOptimizerGet(
       this,
       options,
-      'topColumnsPromise',
-      'topColumns',
+      promise => {
+        this.topColumnsPromise = promise;
+      },
+      () => this.topColumnsPromise,
+      val => {
+        this.topColumns = val;
+      },
       optimizer.fetchTopColumns.bind(optimizer)
     );
   }
@@ -245,8 +257,13 @@ class MultiTableEntry {
     return genericOptimizerGet(
       this,
       options,
-      'topFiltersPromise',
-      'topFilters',
+      promise => {
+        this.topFiltersPromise = promise;
+      },
+      () => this.topFiltersPromise,
+      val => {
+        this.topFilters = val;
+      },
       optimizer.fetchTopFilters.bind(optimizer)
     );
   }
@@ -267,8 +284,13 @@ class MultiTableEntry {
     return genericOptimizerGet(
       this,
       options,
-      'topJoinsPromise',
-      'topJoins',
+      promise => {
+        this.topJoinsPromise = promise;
+      },
+      () => this.topJoinsPromise,
+      val => {
+        this.topJoins = val;
+      },
       optimizer.fetchTopJoins.bind(optimizer)
     );
   }