Bladeren bron

HUE-8768 [editor] Add initial version executor and executableStatement for notebook 2

Johan Ahlen 6 jaren geleden
bovenliggende
commit
eb4331dfa2

+ 113 - 0
desktop/core/src/desktop/js/api/apiHelper.js

@@ -25,6 +25,7 @@ import hueUtils from 'utils/hueUtils';
 
 const AUTOCOMPLETE_API_PREFIX = '/notebook/api/autocomplete/';
 const SAMPLE_API_PREFIX = '/notebook/api/sample/';
+const EXECUTE_API_PREFIX = '/notebook/api/execute/';
 const DOCUMENTS_API = '/desktop/api2/doc/';
 const DOCUMENTS_SEARCH_API = '/desktop/api2/docs/';
 const FETCH_CONFIG = '/desktop/api2/get_config/';
@@ -1795,6 +1796,118 @@ class ApiHelper {
     return new CancellablePromise(deferred, undefined, cancellablePromises);
   }
 
+  /**
+   * API function to execute an ExecutableStatement
+   *
+   * @param {Object} options
+   * @param {boolean} [options.silenceErrors]
+   *
+   * @param {ExecutableStatement} options.executable
+   * @param {ContextCompute} options.compute
+   *
+   * @return {Promise}
+   */
+  execute(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);
+
+    const promise = deferred.promise();
+
+    promise.cancel = () => {
+      const cancelDeferred = $.Deferred();
+      deferred
+        .done(handle => {
+          if (options.executable.handle !== handle) {
+            options.executable.handle = handle;
+          }
+          this.cancelExecute(options).always(cancelDeferred.resolve);
+        })
+        .fail(cancelDeferred.resolve);
+      return cancelDeferred;
+    };
+
+    return promise;
+  }
+
+  cancelExecute(options) {
+    const executable = options.executable;
+
+    // TODO: What do we actually need? And for what reasons....
+    const adaptNotebook2toNotebook = newModel => {
+      const snippet = {
+        type: newModel.sourceType,
+        result: {
+          handle: newModel.handle
+        }
+      };
+
+      return {
+        snippet: JSON.stringify(snippet)
+      };
+    };
+
+    return this.simplePost(
+      '/notebook/api/cancel_statement',
+      adaptNotebook2toNotebook({
+        sourceType: executable.sourceType,
+        handle: executable.handle
+      }),
+      { silenceErrors: options.silenceErrors }
+    );
+  }
+
   /**
    * Fetches samples for the given source and path
    *

+ 7 - 3
desktop/core/src/desktop/js/api/cancellablePromise.js

@@ -45,8 +45,11 @@ class CancellablePromise {
   cancel() {
     const self = this;
     if (self.cancelPrevented || self.cancelled || self.state() !== 'pending') {
-      return;
+      return $.Deferred()
+        .resolve()
+        .promise();
     }
+
     self.cancelled = true;
     if (self.request) {
       apiHelper.cancelActiveRequest(self.request);
@@ -56,10 +59,11 @@ class CancellablePromise {
       self.deferred.reject();
     }
 
+    const cancelPromises = [];
     if (self.otherCancellables) {
       self.otherCancellables.forEach(cancellable => {
         if (cancellable.cancel) {
-          cancellable.cancel();
+          cancelPromises.push(cancellable.cancel());
         }
       });
     }
@@ -67,7 +71,7 @@ class CancellablePromise {
     while (self.cancelCallbacks.length) {
       self.cancelCallbacks.pop()();
     }
-    return self;
+    return $.when(cancelPromises);
   }
 
   onCancel(callback) {

+ 1 - 1
desktop/core/src/desktop/js/apps/notebook/notebook.js

@@ -374,7 +374,7 @@ class Notebook {
             if (self.getSession(session.type()) == null) {
               self.addSession(session);
             } else {
-              var _session = self.getSession(session.type());
+              const _session = self.getSession(session.type());
               komapping.fromJS(data.session, {}, _session);
             }
             $.each(self.getSnippets(session.type()), (index, snippet) => {

+ 5 - 1
desktop/core/src/desktop/js/apps/notebook/snippet.js

@@ -1568,7 +1568,11 @@ class Snippet {
       } else if (data.status == 401) {
         // Auth required
         self.status('expired');
-        $(document).trigger('showAuthModal', { type: self.type(), callback: self.execute, message: data.message });
+        $(document).trigger('showAuthModal', {
+          type: self.type(),
+          callback: self.execute,
+          message: data.message
+        });
       } else if (data.status == 1 || data.status == -1) {
         self.status('failed');
         const match = ERROR_REGEX.exec(data.message);

+ 1 - 0
desktop/core/src/desktop/js/apps/notebook2/editorViewModel.js

@@ -141,6 +141,7 @@ class EditorViewModel {
 
     self.authSessionUsername = ko.observable(); // UI popup
     self.authSessionPassword = ko.observable();
+    self.authSessionMessage = ko.observable();
     self.authSessionType = ko.observable();
     self.authSessionCallback = ko.observable();
 

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

@@ -0,0 +1,99 @@
+// 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 };

+ 136 - 0
desktop/core/src/desktop/js/apps/notebook2/executor.js

@@ -0,0 +1,136 @@
+// 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 { STATUS, ExecutableStatement } from './executableStatement';
+import sqlStatementsParser from 'parse/sqlStatementsParser';
+
+const EXECUTION_FLOW = {
+  step: 'step',
+  batch: 'batch'
+  // batchNoBreak: 'batchNoBreak'
+};
+
+const BATCHABLE_STATEMENT_TYPES = /ALTER|CREATE|DELETE|DROP|GRANT|INSERT|INVALIDATE|LOAD|SET|TRUNCATE|UPDATE|UPSERT|USE/i;
+
+class Executor {
+  /**
+   * @param options
+   * @param {boolean} [options.isSqlEngine] (default false)
+   * @param {string} options.sourceType
+   * @param {ContextCompute} options.compute
+   * @param {ContextNamespace} options.namespace
+   * @param {EXECUTION_FLOW} [options.executionFlow] (default EXECUTION_FLOW.batch)
+   * @param {string} options.statement
+   * @param {string} [options.database]
+   */
+  constructor(options) {
+    this.sourceType = options.sourceType;
+    this.compute = options.compute;
+    this.namespace = options.namespace;
+    this.database = options.database;
+    this.isSqlEngine = options.isSqlEngine;
+    this.executionFlow = this.isSqlEngine
+      ? options.executionFlow || EXECUTION_FLOW.batch
+      : EXECUTION_FLOW.step;
+
+    this.toExecute = [];
+    this.currentExecutable = undefined;
+    this.executed = [];
+
+    if (this.isSqlEngine) {
+      let database = options.database;
+      sqlStatementsParser.parse(options.statement).forEach(parsedStatement => {
+        // If there's no first token it's a trailing comment
+        if (parsedStatement.firstToken) {
+          let skip = false;
+          // TODO: Do we want to send USE statements separately or do we want to send database as param instead?
+          if (/USE/i.test(parsedStatement.firstToken)) {
+            const dbMatch = parsedStatement.statement.match(/use\s+([^;]+)/i);
+            if (dbMatch) {
+              database = dbMatch[1];
+              skip = this.sourceType === 'impala' || this.sourceType === 'hive';
+            }
+          }
+          if (!skip) {
+            this.toExecute.push(
+              new ExecutableStatement({
+                sourceType: options.sourceType,
+                compute: options.compute,
+                namespace: options.namespace,
+                database: database,
+                parsedStatement: parsedStatement
+              })
+            );
+          }
+        }
+      });
+    } else {
+      this.toExecute.push(new ExecutableStatement(options));
+    }
+
+    this.status = STATUS.ready;
+  }
+
+  async cancel() {
+    if (this.currentExecutable && this.currentExecutable.status === STATUS.running) {
+      this.status = STATUS.canceling;
+      return await this.currentExecutable.cancel();
+    }
+  }
+
+  async executeNext() {
+    return new Promise((resolve, reject) => {
+      const executeBatch = () => {
+        if (this.toExecute.length === 0) {
+          this.status = STATUS.success;
+          resolve(this.status);
+        } else {
+          this.status = STATUS.running;
+          this.currentExecutable = this.toExecute.shift();
+          this.currentExecutable.execute().then(() => {
+            this.executed.push(this.currentExecutable);
+            this.currentExecutable = undefined;
+
+            if (this.canExecuteNextInBatch()) {
+              this.executeNext()
+                .then(executeBatch)
+                .catch(reject);
+            } else  {
+              this.status = this.toExecute.length ? STATUS.ready : STATUS.success;
+              resolve(this.status);
+            }
+          }).catch(reject);
+        }
+      };
+
+      executeBatch();
+    });
+  }
+
+  canExecuteNextInBatch() {
+    return (
+      !this.executed.length ||
+      (this.isSqlEngine &&
+        this.executionFlow !== EXECUTION_FLOW.step &&
+        this.currentExecutable &&
+        this.currentExecutable.parsedStatement &&
+        BATCHABLE_STATEMENT_TYPES.test(this.currentExecutable.parsedStatement.firstToken))
+    );
+  }
+}
+
+export default Executor;

+ 4 - 1
desktop/core/src/desktop/js/apps/notebook2/notebook.js

@@ -423,6 +423,9 @@ class Notebook {
           komapping.fromJS(data.session, {}, session);
           if (self.getSession(session.type()) == null) {
             self.addSession(session);
+          } else {
+            const _session = self.getSession(session.type());
+            komapping.fromJS(data.session, {}, _session);
           }
           self.getSnippets(session.type()).forEach(snippet => {
             snippet.status('ready');
@@ -431,7 +434,7 @@ class Notebook {
             setTimeout(callback, 500);
           }
         } else if (data.status === 401) {
-          $(document).trigger('showAuthModal', { type: session.type() });
+          $(document).trigger('showAuthModal', { type: session.type(), message: data.message });
         } else {
           fail(data.message);
         }

+ 11 - 1
desktop/core/src/desktop/js/apps/notebook2/snippet.js

@@ -22,6 +22,8 @@ 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 hueAnalytics from 'utils/hueAnalytics';
 import huePubSub from 'utils/huePubSub';
 import hueUtils from 'utils/hueUtils';
@@ -30,6 +32,10 @@ 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;
+window.Executor = Executor;
+
 const TYPE = {
   hive: 'hive',
   impala: 'impala',
@@ -1977,7 +1983,11 @@ class Snippet {
     } else if (data.status === 401) {
       // Auth required
       self.status(STATUS.expired);
-      $(document).trigger('showAuthModal', { type: self.type(), callback: self.execute });
+      $(document).trigger('showAuthModal', {
+        type: self.type(),
+        callback: self.execute,
+        message: data.message
+      });
     } else if (data.status === 1 || data.status === -1) {
       self.status(STATUS.failed);
       const match = ERROR_REGEX.exec(data.message);

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

@@ -0,0 +1,134 @@
+// 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 { ExecutableStatement, STATUS } from '../executableStatement';
+
+describe('executableStatement.js', () => {
+  /**
+   * @param statement
+   * @return {ExecutableStatement}
+   */
+  const createSubject = statement => {
+    if (typeof statement === 'string') {
+      return new ExecutableStatement({
+        compute: { id: 'compute' },
+        namespace: { id: 'namespace' },
+        database: 'default',
+        statement: statement,
+        sourceType: 'impala'
+      });
+    }
+
+    return new ExecutableStatement({
+      compute: { id: 'compute' },
+      namespace: { id: 'namespace' },
+      database: 'default',
+      parsedStatement: statement,
+      sourceType: 'impala'
+    });
+  };
+
+  it('should handle strings statements', () => {
+    const subject = createSubject('SELECT * FROM customers');
+
+    expect(subject.getStatement()).toEqual('SELECT * FROM customers');
+  });
+
+  it('should handle parsed statements', () => {
+    const subject = createSubject({ statement: 'SELECT * FROM customers' });
+
+    expect(subject.getStatement()).toEqual('SELECT * FROM customers');
+  });
+
+  it('should set the correct status after successful execute', done => {
+    const subject = createSubject('SELECT * FROM customers');
+
+    const simplePostDeferred = $.Deferred();
+    spyOn(ApiHelper, 'simplePost').and.callFake(url => {
+      expect(url).toEqual('/notebook/api/execute/impala');
+      return simplePostDeferred;
+    });
+
+    subject
+      .execute()
+      .then(() => {
+        expect(subject.status).toEqual(STATUS.fetchingResults);
+        done();
+      })
+      .catch(fail);
+
+    expect(subject.status).toEqual(STATUS.running);
+
+    simplePostDeferred.resolve({ handle: {} });
+  });
+
+  it('should set the correct status after failed execute', done => {
+    const subject = createSubject('SELECT * FROM customers');
+
+    const simplePostDeferred = $.Deferred();
+    spyOn(ApiHelper, 'simplePost').and.callFake(url => {
+      expect(url).toEqual('/notebook/api/execute/impala');
+      return simplePostDeferred;
+    });
+
+    subject
+      .execute()
+      .then(fail)
+      .catch(() => {
+        expect(subject.status).toEqual(STATUS.failed);
+        done();
+      });
+
+    expect(subject.status).toEqual(STATUS.running);
+
+    simplePostDeferred.reject();
+  });
+
+  it('should set the correct status when cancelling', done => {
+    const subject = createSubject('SELECT * FROM customers');
+
+    const simplePostExeuteDeferred = $.Deferred();
+    const simplePostCancelDeferred = $.Deferred();
+    spyOn(ApiHelper, 'simplePost').and.callFake(url => {
+      if (url === '/notebook/api/execute/impala') {
+        return simplePostExeuteDeferred;
+      } else if (url === '/notebook/api/cancel_statement') {
+        return simplePostCancelDeferred;
+      }
+      fail();
+    });
+
+    subject
+      .execute()
+      .then(() => {
+        expect(subject.status).toEqual(STATUS.fetchingResults);
+        subject.cancel().then(() => {
+          expect(subject.status).toEqual(STATUS.canceled);
+          done();
+        });
+
+        expect(subject.status).toEqual(STATUS.canceling);
+
+        simplePostCancelDeferred.resolve();
+      })
+      .catch(fail);
+
+    expect(subject.status).toEqual(STATUS.running);
+
+    simplePostExeuteDeferred.resolve({ handle: {} });
+  });
+});

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

@@ -0,0 +1,57 @@
+// 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 { STATUS } from '../executableStatement';
+import Executor from '../executor';
+
+describe('executor.js', () => {
+  /**
+   * @param statement
+   * @return {Executor}
+   */
+  const createSubject = statement =>
+    new Executor({
+      compute: { id: 'compute' },
+      namespace: { id: 'namespace' },
+      database: 'default',
+      sourceType: 'impala',
+      statement: statement,
+      isSqlEngine: true
+    });
+
+  it('should set the correct status after successful execute', done => {
+    const subject = createSubject('SELECT * FROM customers;');
+
+    const simplePostDeferred = $.Deferred();
+    spyOn(ApiHelper, 'simplePost').and.callFake(url => {
+      expect(url).toEqual('/notebook/api/execute/impala');
+      return simplePostDeferred;
+    });
+
+    subject
+      .executeNext()
+      .then(() => {
+        expect(subject.status).toEqual(STATUS.success);
+        done();
+      })
+      .catch(fail);
+
+    expect(subject.status).toEqual(STATUS.running);
+
+    simplePostDeferred.resolve({ handle: {} });
+  });
+});

+ 3 - 0
desktop/core/src/desktop/js/hue.js

@@ -14,6 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import '@babel/polyfill';
 import _ from 'lodash';
 import $ from 'jquery/jquery.common';
 import 'ext/bootstrap.2.3.2.min';
@@ -32,6 +33,8 @@ import sprintf from 'sprintf-js';
 
 import ko from 'ko/ko.all';
 
+import 'parse/parserTypeDefs';
+
 import 'utils/customIntervals';
 import 'utils/json.bigDataParse';
 import apiHelper from 'api/apiHelper';

+ 35 - 0
desktop/core/src/desktop/js/parse/parserTypeDefs.js

@@ -0,0 +1,35 @@
+// 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.
+
+/**
+ * @typedef {Object} ParsedLocation
+ * @property {number} first_line
+ * @property {number} first_column
+ * @property {number} last_line
+ * @property {number} last_column
+ */
+
+/**
+ * @typedef {Object} ParsedSqlStatement
+ * @property {string} firstToken - The first non comment token of the parsed statement, for instance 'select'
+ * @property {string} statement
+ * @property {ParsedLocation} location
+ * @property {string} type - Always 'statement'
+ */
+
+/**
+ * @typedef {ParsedSqlStatement[]} SqlStatementsParserResult
+ */

+ 5 - 0
desktop/core/src/desktop/js/parse/sqlStatementsParser.js

@@ -183,6 +183,11 @@ parseError: function parseError (str, hash) {
         throw error;
     }
 },
+/**
+ * @param {string} input
+ *
+ * @return {SqlStatementsParserResult}
+ */
 parse: function parse(input) {
     var self = this, stack = [0], tstack = [], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1;
     var args = lstack.slice.call(arguments, 1);

+ 9 - 12
package-lock.json

@@ -870,26 +870,23 @@
       }
     },
     "@babel/polyfill": {
-      "version": "7.2.5",
-      "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.2.5.tgz",
-      "integrity": "sha512-8Y/t3MWThtMLYr0YNC/Q76tqN1w30+b0uQMeFUYauG2UGTR19zyUtFrAzT23zNtBxPp+LbE5E/nwV/q/r3y6ug==",
-      "dev": true,
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.4.0.tgz",
+      "integrity": "sha512-bVsjsrtsDflIHp5I6caaAa2V25Kzn50HKPL6g3X0P0ni1ks+58cPB8Mz6AOKVuRPgaVdq/OwEUc/1vKqX+Mo4A==",
       "requires": {
-        "core-js": "^2.5.7",
-        "regenerator-runtime": "^0.12.0"
+        "core-js": "^2.6.5",
+        "regenerator-runtime": "^0.13.2"
       },
       "dependencies": {
         "core-js": {
           "version": "2.6.5",
           "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz",
-          "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==",
-          "dev": true
+          "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A=="
         },
         "regenerator-runtime": {
-          "version": "0.12.1",
-          "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz",
-          "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==",
-          "dev": true
+          "version": "0.13.2",
+          "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz",
+          "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA=="
         }
       }
     },

+ 1 - 0
package.json

@@ -24,6 +24,7 @@
     "node": ">=0.10.0"
   },
   "dependencies": {
+    "@babel/polyfill": "^7.4.0",
     "clipboard": "1.7.1",
     "d3": "5.7.0",
     "d3v3": "1.0.3",

+ 7 - 1
tools/jison/generateParsers.js

@@ -33,6 +33,12 @@ const LICENSE = '// Licensed to Cloudera, Inc. under one\n' +
   '// See the License for the specific language governing permissions and\n' +
   '// limitations under the License.\n';
 
+const SQL_STATEMENTS_PARSER_JSDOC = '/**\n' +
+  ' * @param {string} input\n' +
+  ' *\n' +
+  ' * @return {SqlStatementsParserResult}\n' +
+  ' */\n';
+
 const JISON_FOLDER = 'desktop/core/src/desktop/js/parse/jison/';
 const TARGET_FOLDER = 'desktop/core/src/desktop/js/parse/';
 
@@ -78,7 +84,7 @@ const PARSERS = {
     sources: ['sqlStatementsParser.jison'],
     target: 'sqlStatementsParser.jison',
     afterParse: (contents) => new Promise(resolve => {
-      resolve(LICENSE + contents + 'export default sqlStatementsParser;\n');
+      resolve(LICENSE + contents.replace('parse: function parse', SQL_STATEMENTS_PARSER_JSDOC + 'parse: function parse') + 'export default sqlStatementsParser;\n');
     })
   },
   sqlSyntaxParser: {