Prechádzať zdrojové kódy

HUE-8768 [editor] Implement the execute and check status functionality for notebook 2

Johan Ahlen 6 rokov pred
rodič
commit
baf91bf2f5

+ 153 - 71
desktop/core/src/desktop/js/api/apiHelper.js

@@ -1796,72 +1796,81 @@ class ApiHelper {
     return new CancellablePromise(deferred, undefined, cancellablePromises);
   }
 
+  /**
+   *
+   * @param {ExecutableStatement} executable
+   *
+   * @return {{snippet: string, notebook: string}}
+   */
+  static adaptExecutableToNotebook(executable) {
+    const statement = executable.getStatement();
+    const snippet = {
+      type: executable.sourceType,
+      result: {
+        handle: executable.handle
+      },
+      status: executable.status,
+      id: executable.snippetId || hueUtils.UUID(),
+      statement_raw: statement,
+      statement: statement,
+      variables: [],
+      compute: executable.compute,
+      database: executable.database,
+      properties: { settings: [] },
+    };
+
+    const notebook = {
+      type: executable.sourceType,
+      snippets: [ snippet ],
+      id: executable.notebookId,
+      name: '',
+      isSaved: false,
+      sessions: executable.sessions || []
+    };
+
+    return {
+      snippet: JSON.stringify(snippet),
+      notebook: JSON.stringify(notebook)
+    };
+  }
+
+  /**
+   * @typedef {Object} ExecutionHandle
+   * @property {string} guid
+   * @property {boolean} has_more_statements
+   * @property {boolean} has_result_set
+   * @property {Object} log_context
+   * @property {number} modified_row_count
+   * @property {number} operation_type
+   * @property {string} previous_statement_hash
+   * @property {string} secret
+   * @property {string} session_guid
+   * @property {string} statement
+   * @property {number} statement_id
+   * @property {number} statements_count
+   */
+
   /**
    * API function to execute an ExecutableStatement
    *
    * @param {Object} options
    * @param {boolean} [options.silenceErrors]
-   *
    * @param {ExecutableStatement} options.executable
-   * @param {ContextCompute} options.compute
    *
-   * @return {Promise}
+   * @return {Promise<ExecutionHandle>}
    */
-  execute(options) {
+  executeStatement(options) {
     const executable = options.executable;
     const url = EXECUTE_API_PREFIX + executable.sourceType;
     const deferred = $.Deferred();
 
-    // TODO: What do we actually need? And for what reasons....
-    const adaptNotebook2toNotebook = newModel => {
-      const snippet = {
-        id: newModel.snippetId || hueUtils.UUID(),
-        statement_raw: newModel.statement,
-        type: newModel.sourceType,
-        variables: [],
-        properties: { settings: [] },
-        statement: newModel.statement,
-        result: {
-          handle: newModel.handle
-        }
-      };
-
-      const notebook = {
-        id: newModel.notebookId,
-        type: newModel.sourceType,
-        snippets: [snippet],
-        name: '',
-        isSaved: false,
-        sessions: newModel.sessions
-      };
-
-      return {
-        notebook: JSON.stringify(notebook),
-        snippet: JSON.stringify(snippet)
-      };
-    };
-
-    this.simplePost(
-      url,
-      adaptNotebook2toNotebook({
-        compute: executable.compute,
-        statement: executable.getStatement(),
-        database: executable.database, // Not in use?
-        notebookId: executable.notebookId,
-        sessions: [], // { type: 'spark' } etc.
-        handle: executable.handle,
-        sourceType: executable.sourceType
-      }),
-      options
-    )
-      .done(response => {
-        if (response.handle) {
-          deferred.resolve(response.handle);
-        } else {
-          deferred.reject('No handle in execute response');
-        }
-      })
-      .fail(deferred.reject);
+    this.simplePost(url, ApiHelper.adaptExecutableToNotebook(executable), options).done(response => {
+      if (response.handle) {
+        deferred.resolve(response.handle);
+      } else {
+        deferred.reject('No handle in execute response');
+      }
+    }).fail(deferred.reject);
 
     const promise = deferred.promise();
 
@@ -1872,7 +1881,7 @@ class ApiHelper {
           if (options.executable.handle !== handle) {
             options.executable.handle = handle;
           }
-          this.cancelExecute(options).always(cancelDeferred.resolve);
+          this.cancelStatement(options).always(cancelDeferred.resolve);
         })
         .fail(cancelDeferred.resolve);
       return cancelDeferred;
@@ -1881,30 +1890,103 @@ class ApiHelper {
     return promise;
   }
 
-  cancelExecute(options) {
-    const executable = options.executable;
+  /**
+   *
+   * @param {Object} options
+   * @param {boolean} [options.silenceErrors]
+   * @param {ExecutableStatement} options.executable
+   *
+   * @return {CancellablePromise<string>}
+   */
+  checkExecutionStatus(options) {
+    const deferred = $.Deferred();
 
-    // TODO: What do we actually need? And for what reasons....
-    const adaptNotebook2toNotebook = newModel => {
-      const snippet = {
-        type: newModel.sourceType,
-        result: {
-          handle: newModel.handle
-        }
-      };
+    let request = this.simplePost(
+      '/notebook/api/check_status',
+      ApiHelper.adaptExecutableToNotebook(options.executable),
+      options
+    ).done(response => {
+      deferred.resolve(response.query_status)
+    }).fail(deferred.reject);
+
+    return new CancellablePromise(deferred, request);
+
+  }
 
-      return {
-        snippet: JSON.stringify(snippet)
-      };
-    };
 
+  /**
+   *
+   * @param {Object} options
+   * @param {boolean} [options.silenceErrors]
+   * @param {ExecutableStatement} options.executable
+   *
+   * @return {Promise}
+   */
+  cancelStatement(options) {
     return this.simplePost(
       '/notebook/api/cancel_statement',
-      adaptNotebook2toNotebook({
+      ApiHelper.adaptExecutableToNotebook(options.executable),
+      options
+    );
+  }
+
+  /**
+   * @typedef {Object} ResultResponseMeta
+   * @property {string} comment
+   * @property {string} name
+   * @property {string} type
+   */
+
+  /**
+   * @typedef {Object} ResultResponse
+   * @property {Object[]} data
+   * @property {boolean} has_more
+   * @property {boolean} isEscaped
+   * @property {ResultResponseMeta[]} meta
+   * @property {string} type
+   */
+
+  /**
+   *
+   * @param {Object} options
+   * @param {boolean} [options.silenceErrors]
+   * @param {ExecutableStatement} options.executable
+   * @param {number} options.rows
+   * @param {boolean} options.startOver
+   *
+   * @return {Promise<ResultResponse>}
+   */
+  async fetchResults(options) {
+    return new Promise((resolve, reject) => {
+
+      const data = ApiHelper.adaptExecutableToNotebook(options.executable);
+      data.rows = options.rows;
+      data.startOver = !!options.startOver;
+
+      this.simplePost('/notebook/api/fetch_result_data', data, options).done((response) => {
+        resolve(response.result);
+      }).fail(reject);
+    })
+  }
+
+  /**
+   *
+   * @param {Object} options
+   * @param {boolean} [options.silenceErrors]
+   * @param {ExecutableStatement} options.executable
+   *
+   * @return {Promise}
+   */
+  closeStatement(options) {
+    const executable = options.executable;
+
+    return this.simplePost(
+      '/notebook/api/close_statement',
+      ApiHelper.adaptExecutableToNotebook({
         sourceType: executable.sourceType,
         handle: executable.handle
       }),
-      { silenceErrors: options.silenceErrors }
+      options
     );
   }
 

+ 1 - 0
desktop/core/src/desktop/js/api/cancellablePromise.js

@@ -14,6 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import $ from 'jquery';
 import apiHelper from 'api/apiHelper';
 
 class CancellablePromise {

+ 0 - 99
desktop/core/src/desktop/js/apps/notebook2/executableStatement.js

@@ -1,99 +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 apiHelper from 'api/apiHelper';
-import hueAnalytics from 'utils/hueAnalytics';
-
-const STATUS = {
-  canceled: 'canceled',
-  canceling: 'canceling',
-  executed: 'executed',
-  fetchingResults: 'fetchingResults',
-  ready: 'ready',
-  running: 'running',
-  success: 'success',
-  failed: 'failed'
-};
-
-class ExecutableStatement {
-  /**
-   * @param options
-   * @param {string} options.sourceType
-   * @param {ContextCompute} options.compute
-   * @param {ContextNamespace} options.namespace
-   * @param {string} [options.statement] - Either supply a statement or a parsedStatement
-   * @param {SqlStatementsParserResult} [options.parsedStatement] - Either supply a statement or a parsedStatement
-   * @param {string} [options.database]
-   */
-  constructor(options) {
-    this.compute = options.compute;
-    this.database = options.database;
-    this.namespace = options.namespace;
-    this.sourceType = options.sourceType;
-    this.parsedStatement = options.parsedStatement;
-    this.statement = options.statement;
-    this.handle = {
-      statement_id: 0 // TODO: Get rid of need for initial handle in the backend
-    };
-
-    this.lastCancellable = undefined;
-    this.status = STATUS.ready;
-  }
-
-  getStatement() {
-    return this.statement || this.parsedStatement.statement;
-  }
-
-  async execute() {
-    if (this.status === STATUS.running) {
-      return;
-    }
-    hueAnalytics.log('notebook', 'execute/' + this.sourceType);
-    this.status = STATUS.running;
-
-    this.lastCancellable = apiHelper
-      .execute({
-        executable: this
-      })
-      .done(handle => {
-        this.handle = handle;
-        this.status = STATUS.fetchingResults;
-      })
-      .fail(error => {
-        this.status = STATUS.failed;
-      });
-
-    return this.lastCancellable;
-  }
-
-  async cancel() {
-    return new Promise(resolve => {
-      if (this.lastCancellable && this.status === STATUS.fetchingResults) {
-        hueAnalytics.log('notebook', 'cancel/' + this.sourceType);
-        this.status = STATUS.canceling;
-        this.lastCancellable.cancel().always(() => {
-          this.status = STATUS.canceled;
-          resolve();
-        });
-        this.lastCancellable = undefined;
-      } else {
-        resolve();
-      }
-    });
-  }
-}
-
-export { STATUS, ExecutableStatement };

+ 170 - 0
desktop/core/src/desktop/js/apps/notebook2/execution/executableStatement.js

@@ -0,0 +1,170 @@
+// 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 apiHelper from 'api/apiHelper';
+import { ExecutionResult } from "apps/notebook2/execution/executionResult";
+import hueAnalytics from 'utils/hueAnalytics';
+
+/**
+ *  ready +----> executing +----> done +----> closed
+ *                   +     |
+ *                   |     +----> fail
+ *                   |
+ *                   +----> canceling +----> canceled
+ *
+ * @type { { canceling: string, canceled: string, fail: string, ready: string, executing: string, done: string } }
+ */
+const EXECUTION_STATUS = {
+  ready: 'ready',
+  executing: 'executing',
+  canceled: 'canceled',
+  canceling: 'canceling',
+  closed: 'closed',
+  done: 'done',
+  fail: 'fail'
+};
+
+class ExecutableStatement {
+
+  /**
+   * @param options
+   * @param {string} options.sourceType
+   * @param {ContextCompute} options.compute
+   * @param {ContextNamespace} options.namespace
+   * @param {string} [options.statement] - Either supply a statement or a parsedStatement
+   * @param {SqlStatementsParserResult} [options.parsedStatement] - Either supply a statement or a parsedStatement
+   * @param {string} [options.database]
+   */
+  constructor(options) {
+    this.compute = options.compute;
+    this.database = options.database;
+    this.namespace = options.namespace;
+    this.sourceType = options.sourceType;
+    this.parsedStatement = options.parsedStatement;
+    this.statement = options.statement;
+    this.handle = {
+      statement_id: 0 // TODO: Get rid of need for initial handle in the backend
+    };
+
+    this.executionResult = undefined;
+    this.lastCancellable = undefined;
+    this.status = EXECUTION_STATUS.ready;
+    this.progress = 0;
+  }
+
+  getStatement() {
+    return this.statement || this.parsedStatement.statement;
+  }
+
+  async execute() {
+    return new Promise((resolve, reject) => {
+      if (this.status !== EXECUTION_STATUS.ready) {
+        reject();
+        return;
+      }
+
+      let statusCheckCount = 0;
+      let checkStatusTimeout = -1;
+
+      const checkStatus = () => new Promise( (resolve, reject) => {
+        statusCheckCount++;
+        this.lastCancellable = apiHelper.checkExecutionStatus({ executable: this }).done(queryStatus => {
+          switch (queryStatus) {
+            case 'success':
+              this.progress = 99; // TODO: why 99 here (from old code)?
+              resolve();
+              break;
+            case 'available':
+              this.progress = 100;
+              resolve();
+              break;
+            case 'expired':
+              reject();
+              break;
+            case 'running':
+            case 'starting':
+            case 'waiting':
+              checkStatusTimeout = window.setTimeout(() => {
+                checkStatus().then(resolve).catch(reject);
+              }, statusCheckCount > 45 ? 5000 : 1000);
+              break;
+            default:
+              console.warn('Got unknown status ' + queryStatus);
+              reject();
+          }
+        }).fail(reject);
+
+        this.lastCancellable.onCancel(() => {
+          window.clearTimeout(checkStatusTimeout);
+        })
+      });
+
+      hueAnalytics.log('notebook', 'execute/' + this.sourceType);
+      this.status = EXECUTION_STATUS.executing;
+
+      this.lastCancellable = apiHelper
+        .executeStatement({
+          executable: this
+        })
+        .done(handle => {
+          this.handle = handle;
+
+          checkStatus().then(() => {
+            this.result = new ExecutionResult(this);
+            this.status = EXECUTION_STATUS.done;
+            resolve(this.result);
+          }).catch(error => {
+            this.status = EXECUTION_STATUS.fail;
+            reject(error);
+          });
+        })
+        .fail(error => {
+          this.status = EXECUTION_STATUS.fail;
+          reject(error);
+        });
+    })
+  }
+
+  async cancel() {
+    return new Promise(resolve => {
+      if (this.lastCancellable && this.status === EXECUTION_STATUS.executing) {
+        hueAnalytics.log('notebook', 'cancel/' + this.sourceType);
+        this.status = EXECUTION_STATUS.canceling;
+        this.lastCancellable.cancel().always(() => {
+          this.status = EXECUTION_STATUS.canceled;
+          resolve();
+        });
+        this.lastCancellable = undefined;
+      } else {
+        resolve();
+      }
+    });
+  }
+  
+  async close() {
+    return new Promise(resolve => {
+      if (this.status === EXECUTION_STATUS.executing) {
+        this.cancel().finally(resolve)
+      } else if (this.status === EXECUTION_STATUS.done) {
+        apiHelper.closeStatement({ executable: this }).finally(resolve);
+      }
+    }).finally(() => {
+      this.status = EXECUTION_STATUS.closed;
+    });
+  }
+}
+
+export { EXECUTION_STATUS, ExecutableStatement };

+ 87 - 0
desktop/core/src/desktop/js/apps/notebook2/execution/executionResult.js

@@ -0,0 +1,87 @@
+// 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 apiHelper from 'api/apiHelper';
+import hueAnalytics from 'utils/hueAnalytics';
+
+/**
+ *  available +----> fetching +----> done
+ *      ^                     |
+ *      |                     +----> fail
+ *      |                     |
+ *      +---------------------+
+ *
+ * If the handle indidcates that there's no result set available the ExecutionResult will have initial status set to
+ * RESULT_STATUS.done
+ *
+ * @type { { canceling: string, canceled: string, fail: string, ready: string, executing: string, done: string } }
+ */
+const RESULT_STATUS = {
+  ready: 'ready',
+  available: 'available',
+  fetching: 'fetching',
+  done: 'done',
+  fail: 'fail'
+};
+
+class ExecutionResult {
+
+  /**
+   *
+   * @param {ExecutableStatement} executable
+   */
+  constructor(executable) {
+    this.executable = executable;
+    this.status = executable.handle.has_result_set ? RESULT_STATUS.available : RESULT_STATUS.done;
+  }
+
+  /**
+   * Fetches additional rows
+   *
+   * @param {Object} options
+   * @param {number} options.rows
+   * @param {boolean} [options.startOver]
+   *
+   * @return {Promise}
+   */
+  async fetch(options) {
+    return new Promise((resolve, reject) => {
+      if (this.status !== RESULT_STATUS.available) {
+        reject();
+        return;
+      }
+      this.status = RESULT_STATUS.fetching;
+      apiHelper.fetchResults({
+        executable: this.executable,
+        rows: options.rows,
+        startOver: !!options.startOver
+      }).then((resultResponse) => {
+        if (resultResponse.has_more) {
+          this.status = RESULT_STATUS.available;
+        } else {
+          this.status = RESULT_STATUS.done;
+        }
+        console.log(resultResponse);
+        resolve(resultResponse);
+      }).catch((error) => {
+        this.status = RESULT_STATUS.fail;
+        reject(error);
+      })
+    })
+  }
+}
+
+export { RESULT_STATUS, ExecutionResult };

+ 20 - 8
desktop/core/src/desktop/js/apps/notebook2/executor.js → desktop/core/src/desktop/js/apps/notebook2/execution/executor.js

@@ -14,9 +14,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import CancellablePromise from 'api/cancellablePromise';
-import { STATUS, ExecutableStatement } from './executableStatement';
+import { EXECUTION_STATUS, ExecutableStatement } from './executableStatement';
 import sqlStatementsParser from 'parse/sqlStatementsParser';
+import huePubSub from "utils/huePubSub";
 
 const EXECUTION_FLOW = {
   step: 'step',
@@ -34,6 +34,7 @@ class Executor {
    * @param {ContextCompute} options.compute
    * @param {ContextNamespace} options.namespace
    * @param {EXECUTION_FLOW} [options.executionFlow] (default EXECUTION_FLOW.batch)
+   * @param {Session} [options.session]
    * @param {string} options.statement
    * @param {string} [options.database]
    */
@@ -82,12 +83,23 @@ class Executor {
       this.toExecute.push(new ExecutableStatement(options));
     }
 
-    this.status = STATUS.ready;
+    this.setStatus(EXECUTION_STATUS.ready);
+    this.setProgress(0);
+  }
+
+  setStatus(status) {
+    this.status = status;
+    huePubSub.publish('hue.executor.status.updated', this);
+  }
+
+  setProgress(progress) {
+    this.progress = progress;
+    huePubSub.publish('hue.executor.progress.updated', this);
   }
 
   async cancel() {
-    if (this.currentExecutable && this.currentExecutable.status === STATUS.running) {
-      this.status = STATUS.canceling;
+    if (this.currentExecutable && this.currentExecutable.status === EXECUTION_STATUS.running) {
+      this.setStatus(EXECUTION_STATUS.canceling);
       return await this.currentExecutable.cancel();
     }
   }
@@ -96,11 +108,11 @@ class Executor {
     return new Promise((resolve, reject) => {
       const executeBatch = () => {
         if (this.toExecute.length === 0) {
-          this.status = STATUS.success;
+          this.setStatus(EXECUTION_STATUS.success);
           resolve(this.status);
         } else {
-          this.status = STATUS.running;
           this.currentExecutable = this.toExecute.shift();
+          this.setStatus(EXECUTION_STATUS.running);
           this.currentExecutable.execute().then(() => {
             this.executed.push(this.currentExecutable);
             this.currentExecutable = undefined;
@@ -110,7 +122,7 @@ class Executor {
                 .then(executeBatch)
                 .catch(reject);
             } else  {
-              this.status = this.toExecute.length ? STATUS.ready : STATUS.success;
+              this.setStatus(this.toExecute.length ? EXECUTION_STATUS.ready : EXECUTION_STATUS.success);
               resolve(this.status);
             }
           }).catch(reject);

+ 28 - 0
desktop/core/src/desktop/js/apps/notebook2/execution/sessionManager.js

@@ -0,0 +1,28 @@
+// 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 { EXECUTION_STATUS, ExecutableStatement } from './executableStatement';
+import sqlStatementsParser from 'parse/sqlStatementsParser';
+import huePubSub from "utils/huePubSub";
+
+const knownSessions {
+  impala: {...},
+  presto: {...}
+}
+
+getSession(interpreter) {
+
+}

+ 0 - 0
desktop/core/src/desktop/js/apps/notebook2/spec/executableStatementSpec.js → desktop/core/src/desktop/js/apps/notebook2/execution/spec/executableStatementSpec.js


+ 0 - 0
desktop/core/src/desktop/js/apps/notebook2/spec/executorSpec.js → desktop/core/src/desktop/js/apps/notebook2/execution/spec/executorSpec.js


+ 1 - 9
desktop/core/src/desktop/js/apps/notebook2/result.js

@@ -50,17 +50,9 @@ const isStringColumn = type =>
   !isNumericColumn(type) && !isDateTimeColumn(type) && !isComplexColumn(type);
 
 class Result {
-  constructor(snippet, result) {
+  constructor(result) {
     const self = this;
 
-    $.extend(
-      snippet,
-      snippet.chartType === 'lines' && {
-        // Retire line chart
-        chartType: 'bars',
-        chartTimelineType: 'line'
-      }
-    );
     self.id = ko.observable(result.id || hueUtils.UUID());
     self.type = ko.observable(result.type || 'table');
     self.hasResultset = ko.observable(result.hasResultset !== false).extend('throttle', 100);

+ 65 - 195
desktop/core/src/desktop/js/apps/notebook2/snippet.js

@@ -22,15 +22,14 @@ import { markdown } from 'markdown';
 import AceAutocompleteWrapper from 'apps/notebook/aceAutocompleteWrapper';
 import apiHelper from 'api/apiHelper';
 import dataCatalog from 'catalog/dataCatalog';
-import { ExecutableStatement } from "./executableStatement";
-import Executor from './executor';
+import { ExecutableStatement } from 'apps/notebook2/execution/executableStatement';
+import Executor from 'apps/notebook2/execution/executor';
 import hueAnalytics from 'utils/hueAnalytics';
 import huePubSub from 'utils/huePubSub';
 import hueUtils from 'utils/hueUtils';
 import { NOTEBOOK_MAPPING } from 'apps/notebook2/notebook';
 import Result from 'apps/notebook2/result';
 import Session from 'apps/notebook2/session';
-import sqlStatementsParser from 'parse/sqlStatementsParser';
 
 // TODO: Remove. Temporary here for debug
 window.ExecutableStatement = ExecutableStatement;
@@ -724,7 +723,16 @@ class Snippet {
       return statement;
     });
 
-    self.result = new Result(snippet, snippet.result);
+    $.extend(
+      snippet,
+      snippet.chartType === 'lines' && {
+        // Retire line chart
+        chartType: 'bars',
+        chartTimelineType: 'line'
+      }
+    );
+
+    self.result = new Result(snippet.result);
     if (!self.result.hasSomeResults()) {
       self.currentQueryTab('queryHistory');
     }
@@ -834,6 +842,7 @@ class Snippet {
       $(document).trigger('forceChartDraw', self);
     });
 
+
     self.previousChartOptions = {};
 
     self.result.meta.subscribe(() => {
@@ -1088,6 +1097,22 @@ class Snippet {
       optEnabled: false,
       timeout: self.parentVm.autocompleteTimeout
     });
+
+    self.executor = undefined;
+
+    const updateExecutorObservable = (executor, name) => {
+      if (executor === self.executor && self[name]() !== executor[name]) {
+        self[name](executor[name]);
+      }
+    };
+
+    huePubSub.subscribe('hue.executor.status.updated', executor => {
+      updateExecutorObservable(executor, 'status');
+    });
+
+    huePubSub.subscribe('hue.executor.progress.updated', executor => {
+      updateExecutorObservable(executor, 'progress');
+    })
   }
 
   ace(newVal) {
@@ -1281,210 +1306,55 @@ class Snippet {
   }
 
   execute(automaticallyTriggered) {
-    const self = this;
-    if (!automaticallyTriggered && self.ace()) {
-      const selectionRange = self.ace().getSelectionRange();
-
-      if (
-        self.lastExecutedSelectionRange &&
-        (selectionRange.start.row !== selectionRange.end.row &&
-          selectionRange.start.column !== selectionRange.end.column) &&
-        (selectionRange.start.row !== self.lastExecutedSelectionRange.start.row ||
-          selectionRange.start.column !== self.lastExecutedSelectionRange.start.column ||
-          selectionRange.end.row !== self.lastExecutedSelectionRange.end.row ||
-          selectionRange.end.column !== self.lastExecutedSelectionRange.end.column)
-      ) {
-        // Manual execute and there is a selection that is different from the last execute
-        self.result.cancelBatchExecution();
-      }
-      self.lastExecutedSelectionRange = selectionRange;
-    }
+    hueAnalytics.log('notebook', 'execute/' + this.type());
 
-    if (self.isCanceling()) {
-      return;
-    }
     const now = new Date().getTime();
-    if (now - self.lastExecuted() < 1000 || !self.isReady()) {
+    if (now - this.lastExecuted() < 1000) {
       return; // Prevent fast clicks
     }
+    this.lastExecuted(now);
 
-    if (!automaticallyTriggered) {
-      // Do not cancel statements that are parts of a set of steps to execute (e.g. import). Only cancel statements as requested by user
-      if (self.status() === STATUS.running || self.status() === STATUS.loading) {
-        self.cancel(); // TODO: Wait for cancel to finish
-      } else {
-        self.result.clear();
-      }
-    }
-
-    if (self.type() === TYPE.impala) {
-      self.showExecutionAnalysis(false);
+    if (this.type() === TYPE.impala) {
+      this.showExecutionAnalysis(false);
       huePubSub.publish('editor.clear.execution.analysis');
     }
 
-    self.status(STATUS.running);
-    self.statusForButtons(STATUS_FOR_BUTTONS.executing);
-
-    if (self.isSqlDialect()) {
-      huePubSub.publish('editor.refresh.statement.locations', self);
-    }
+    // Editor based execution
+    if (this.ace()) {
+      const selectionRange = this.ace().getSelectionRange();
 
-    self.lastExecutedStatements = self.statement();
+      if (this.isSqlDialect()) {
+        huePubSub.publish('editor.refresh.statement.locations', this);
+      }
 
-    if (self.ace()) {
-      huePubSub.publish('ace.set.autoexpand', { autoExpand: false, snippet: self });
-      const selectionRange = self.ace().getSelectionRange();
-      self.lastAceSelectionRowOffset(Math.min(selectionRange.start.row, selectionRange.end.row));
+      huePubSub.publish('ace.set.autoexpand', { autoExpand: false, snippet: this });
+      this.lastAceSelectionRowOffset(Math.min(selectionRange.start.row, selectionRange.end.row));
     }
 
-    self.previousChartOptions = self.parentVm.getPreviousChartOptions(self);
-    $(document).trigger('executeStarted', { vm: self.parentVm, snippet: self });
-    self.lastExecuted(now);
+    this.previousChartOptions = this.parentVm.getPreviousChartOptions(this);
+    $(document).trigger('executeStarted', { vm: this.parentVm, snippet: this });
     $('.jHueNotify').remove();
-    hueAnalytics.log('notebook', 'execute/' + self.type());
-
-    self.parentNotebook.forceHistoryInitialHeight(true);
-
-    if (self.result.handle()) {
-      self.close();
-    }
-
-    self.errors([]);
-    huePubSub.publish('editor.clear.highlighted.errors', self.ace());
-    self.result.clear();
-    self.progress(0);
-    self.jobs([]);
-    self.result.logs('');
-    self.result.statement_range({
-      start: {
-        row: 0,
-        column: 0
-      },
-      end: {
-        row: 0,
-        column: 0
-      }
-    });
-    self.parentNotebook.historyCurrentPage(1);
-
-    // TODO: rename startLongOperationTimeout to startBlockingOperationTimeout
-    // TODO: stop blocking operation UI if there is one
-    // TODO: offer to stop blocking submit or fetch operation UI if there is one (add a new call to function for cancelBlockingOperation)
-    // TODO: stop current blocking operation if there is one
-    // TODO: handle jquery.dataTables.1.8.2.min.js:150 Uncaught TypeError: Cannot read property 'asSorting' of undefined on some cancels
-    // TODO: we should cancel blocking operation when leaving notebook (similar to unload())
-    // TODO: we should test when we go back to a query history of a blocking operation that we left
-    self.startLongOperationTimeout();
-
-    self.currentQueryTab('queryHistory');
-
-    self.executingBlockingOperation = $.post(
-      '/notebook/api/execute/' + self.type(),
-      {
-        notebook: self.parentVm.editorMode()
-          ? komapping.toJSON(self.parentNotebook, NOTEBOOK_MAPPING)
-          : komapping.toJSON(self.parentNotebook.getContext(), NOTEBOOK_MAPPING),
-        snippet: komapping.toJSON(self.getContext())
-      },
-      data => {
-        try {
-          if (self.isSqlDialect() && data && data.handle) {
-            self.lastExecutedStatement(sqlStatementsParser.parse(data.handle.statement)[0]);
-          } else {
-            self.lastExecutedStatement(null);
-          }
-        } catch (e) {
-          self.lastExecutedStatement(null);
-        }
-        self.statusForButtons(STATUS_FOR_BUTTONS.executed);
-        huePubSub.publish('ace.set.autoexpand', { autoExpand: true, snippet: self });
-        self.stopLongOperationTimeout();
-
-        if (self.parentVm.editorMode() && data.history_id) {
-          if (!self.parentVm.isNotificationManager()) {
-            const url = self.parentVm.URLS.editor + '?editor=' + data.history_id;
-            self.parentVm.changeURL(url);
-          }
-          self.parentNotebook.id(data.history_id);
-          self.parentNotebook.uuid(data.history_uuid);
-          self.parentNotebook.isHistory(true);
-          self.parentNotebook.parentSavedQueryUuid(data.history_parent_uuid);
-        }
-
-        if (data.status === 0) {
-          self.result.handle(data.handle);
-          self.result.hasResultset(data.handle.has_result_set);
-          if (data.handle.sync) {
-            self.loadData(data.result, 100);
-            self.status(STATUS.available);
-            self.progress(100);
-            self.result.endTime(new Date());
-          } else if (!self.parentNotebook.unloaded()) {
-            self.checkStatus();
-          }
-          if (self.parentVm.isOptimizerEnabled()) {
-            huePubSub.publish('editor.upload.query', data.history_id);
-          }
-        } else {
-          self.handleAjaxError(data, self.execute);
-          self.parentNotebook.isExecutingAll(false);
-        }
-
-        if (data.handle) {
-          if (self.parentVm.editorMode()) {
-            if (self.parentVm.isNotificationManager()) {
-              // Update task status
-              const tasks = self.parentNotebook
-                .history()
-                .filter(row => row.uuid() === self.parentNotebook.uuid());
-              if (tasks.length === 1) {
-                tasks[0].status(self.status());
-                self.result.logs(data.message);
-              }
-            } else {
-              self.parentNotebook.history.unshift(
-                self.parentNotebook.makeHistoryRecord(
-                  undefined,
-                  data.handle.statement,
-                  self.lastExecuted(),
-                  self.status(),
-                  self.parentNotebook.name(),
-                  self.parentNotebook.uuid()
-                )
-              );
-            }
-          }
-
-          if (data.handle.statements_count != null) {
-            self.result.statements_count(data.handle.statements_count);
-            self.result.statement_id(data.handle.statement_id);
-            self.result.previous_statement_hash(data.previous_statement_hash);
-
-            if (
-              data.handle.statements_count > 1 &&
-              data.handle.start != null &&
-              data.handle.end != null
-            ) {
-              self.result.statement_range({
-                start: data.handle.start,
-                end: data.handle.end
-              });
-            }
-          }
-        }
-      }
-    )
-      .fail(xhr => {
-        if (self.statusForButtons() !== STATUS_FOR_BUTTONS.canceled && xhr.status !== 502) {
-          // No error when manually canceled
-          $(document).trigger('error', xhr.responseText);
-        }
-        self.status(STATUS.failed);
-        self.statusForButtons(STATUS_FOR_BUTTONS.executed);
-      })
-      .always(() => {
-        self.executingBlockingOperation = null;
-      });
+    this.parentNotebook.forceHistoryInitialHeight(true);
+    this.errors([]);
+    huePubSub.publish('editor.clear.highlighted.errors', this.ace());
+    this.result.clear();
+    this.progress(0);
+    this.jobs([]);
+
+    this.parentNotebook.historyCurrentPage(1);
+
+    this.startLongOperationTimeout();
+
+    this.currentQueryTab('queryHistory');
+
+    this.executor = new Executor({
+      compute: this.compute(),
+      database: this.database(),
+      sourceType: this.type(),
+      namespace: this.namespace(),
+      statement: this.statement(),
+      isSqlEngine: this.isSqlDialect()
+    }).executeNext();
   }
 
   explain() {

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

@@ -640,6 +640,7 @@ class DataCatalog {
    * @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) {