Pārlūkot izejas kodu

[frontend] Merge Executable into SqlExecutable

This takes care of an issue with inheritance of abstract classes in web components. In reality we only have one type of executable so there's no actual need for the abstraction. When and if we need more executables it's better to use an interface.
Johan Åhlén 4 gadi atpakaļ
vecāks
revīzija
4e395545a2
24 mainītis faili ar 550 papildinājumiem un 606 dzēšanām
  1. 1 1
      desktop/core/src/desktop/js/apps/editor/api.ts
  2. 5 5
      desktop/core/src/desktop/js/apps/editor/components/ExecutableProgressBar.test.ts
  3. 4 4
      desktop/core/src/desktop/js/apps/editor/components/ExecutableProgressBar.vue
  4. 2 3
      desktop/core/src/desktop/js/apps/editor/components/ExecuteButton.test.ts
  5. 1 2
      desktop/core/src/desktop/js/apps/editor/components/ExecuteButton.vue
  6. 1 1
      desktop/core/src/desktop/js/apps/editor/components/ExecutionStatusIcon.vue
  7. 1 1
      desktop/core/src/desktop/js/apps/editor/components/QueryHistoryTable.vue
  8. 2 2
      desktop/core/src/desktop/js/apps/editor/components/events.ts
  9. 3 4
      desktop/core/src/desktop/js/apps/editor/components/executionAnalysis/ExecutionAnalysisPanel.vue
  10. 3 3
      desktop/core/src/desktop/js/apps/editor/components/presentationMode/PresentationModeKoBridge.vue
  11. 3 3
      desktop/core/src/desktop/js/apps/editor/components/result/ResultTable.vue
  12. 1 1
      desktop/core/src/desktop/js/apps/editor/components/resultChart/ko.resultChart.js
  13. 1 1
      desktop/core/src/desktop/js/apps/editor/components/resultGrid/ko.simpleResultGrid.js
  14. 1 2
      desktop/core/src/desktop/js/apps/editor/components/sqlScratchpad/SqlScratchpad.vue
  15. 7 5
      desktop/core/src/desktop/js/apps/editor/execution/api.ts
  16. 3 3
      desktop/core/src/desktop/js/apps/editor/execution/events.ts
  17. 0 493
      desktop/core/src/desktop/js/apps/editor/execution/executable.ts
  18. 3 3
      desktop/core/src/desktop/js/apps/editor/execution/executionLogs.ts
  19. 3 3
      desktop/core/src/desktop/js/apps/editor/execution/executionResult.ts
  20. 6 6
      desktop/core/src/desktop/js/apps/editor/execution/executor.ts
  21. 1 1
      desktop/core/src/desktop/js/apps/editor/execution/sqlExecutable.test.ts
  22. 496 55
      desktop/core/src/desktop/js/apps/editor/execution/sqlExecutable.ts
  23. 1 2
      desktop/core/src/desktop/js/apps/editor/execution/utils.ts
  24. 1 2
      desktop/core/src/desktop/js/apps/editor/snippet.js

+ 1 - 1
desktop/core/src/desktop/js/apps/editor/api.ts

@@ -15,7 +15,7 @@
 // limitations under the License.
 
 import { format } from '@gethue/sql-formatter';
-import { ExecutionStatus } from './execution/executable';
+import { ExecutionStatus } from './execution/sqlExecutable';
 import { CancellablePromise } from 'api/cancellablePromise';
 import { get } from 'api/utils';
 

+ 5 - 5
desktop/core/src/desktop/js/apps/editor/components/ExecutableProgressBar.test.ts

@@ -17,7 +17,7 @@
 import { EXECUTABLE_UPDATED_TOPIC, ExecutableUpdatedEvent } from 'apps/editor/execution/events';
 import { nextTick } from 'vue';
 import { shallowMount, mount } from '@vue/test-utils';
-import Executable, { ExecutionStatus } from 'apps/editor/execution/executable';
+import SqlExecutable, { ExecutionStatus } from 'apps/editor/execution/sqlExecutable';
 import ExecutableProgressBar from './ExecutableProgressBar.vue';
 import huePubSub from 'utils/huePubSub';
 
@@ -36,7 +36,7 @@ describe('ExecutableProgressBar.vue', () => {
 
     const { element } = mount(ExecutableProgressBar, {
       propsData: {
-        executable: <Executable>mockExecutable
+        executable: <SqlExecutable>mockExecutable
       }
     });
 
@@ -49,7 +49,7 @@ describe('ExecutableProgressBar.vue', () => {
     mockExecutable.progress = 10;
     huePubSub.publish<ExecutableUpdatedEvent>(
       EXECUTABLE_UPDATED_TOPIC,
-      mockExecutable as Executable
+      mockExecutable as SqlExecutable
     );
     await nextTick();
 
@@ -65,7 +65,7 @@ describe('ExecutableProgressBar.vue', () => {
 
     const { element } = mount(ExecutableProgressBar, {
       propsData: {
-        executable: <Executable>mockExecutable
+        executable: <SqlExecutable>mockExecutable
       }
     });
 
@@ -78,7 +78,7 @@ describe('ExecutableProgressBar.vue', () => {
     mockExecutable.progress = 10;
     huePubSub.publish<ExecutableUpdatedEvent>(
       EXECUTABLE_UPDATED_TOPIC,
-      mockExecutable as Executable
+      mockExecutable as SqlExecutable
     );
     await nextTick();
 

+ 4 - 4
desktop/core/src/desktop/js/apps/editor/components/ExecutableProgressBar.vue

@@ -33,13 +33,13 @@
   import { defineComponent, PropType, ref, toRefs, watch } from 'vue';
 
   import SubscriptionTracker from 'components/utils/SubscriptionTracker';
-  import Executable, { ExecutionStatus } from '../execution/executable';
+  import SqlExecutable, { ExecutionStatus } from '../execution/sqlExecutable';
 
   export default defineComponent({
     name: 'ExecutableProgressBar',
     props: {
       executable: {
-        type: Object as PropType<Executable>,
+        type: Object as PropType<SqlExecutable>,
         default: undefined
       }
     },
@@ -53,7 +53,7 @@
 
       let hideTimeout = -1;
 
-      const updateFromExecutable = (updated?: Executable) => {
+      const updateFromExecutable = (updated?: SqlExecutable) => {
         window.clearTimeout(hideTimeout);
         progress.value = (updated && updated.progress) || 0;
         status.value = (updated && updated.status) || ExecutionStatus.ready;
@@ -69,7 +69,7 @@
       watch(
         executable,
         newVal => {
-          updateFromExecutable(newVal as Executable);
+          updateFromExecutable(newVal as SqlExecutable);
         },
         { immediate: true }
       );

+ 2 - 3
desktop/core/src/desktop/js/apps/editor/components/ExecuteButton.test.ts

@@ -15,11 +15,10 @@
 // limitations under the License.
 
 import { EXECUTABLE_UPDATED_TOPIC, ExecutableUpdatedEvent } from 'apps/editor/execution/events';
-import SqlExecutable from 'apps/editor/execution/sqlExecutable';
 import huePubSub from 'utils/huePubSub';
 import { nextTick } from 'vue';
 import { mount, shallowMount } from '@vue/test-utils';
-import Executable, { ExecutionStatus } from 'apps/editor/execution/executable';
+import SqlExecutable, { ExecutionStatus } from 'apps/editor/execution/sqlExecutable';
 import sessionManager from 'apps/editor/execution/sessionManager';
 import ExecuteButton from './ExecuteButton.vue';
 import noop from 'utils/timing/noop';
@@ -115,7 +114,7 @@ describe('ExecuteButton.vue', () => {
     mockExecutable.status = ExecutionStatus.running;
     huePubSub.publish<ExecutableUpdatedEvent>(
       EXECUTABLE_UPDATED_TOPIC,
-      mockExecutable as Executable
+      mockExecutable as SqlExecutable
     );
 
     await nextTick();

+ 1 - 2
desktop/core/src/desktop/js/apps/editor/components/ExecuteButton.vue

@@ -65,9 +65,8 @@
     ExecutableTransitionedEvent,
     ExecutableUpdatedEvent
   } from 'apps/editor/execution/events';
-  import { ExecutionStatus } from 'apps/editor/execution/executable';
   import sessionManager from 'apps/editor/execution/sessionManager';
-  import SqlExecutable from 'apps/editor/execution/sqlExecutable';
+  import SqlExecutable, { ExecutionStatus } from 'apps/editor/execution/sqlExecutable';
   import HueButton from 'components/HueButton.vue';
   import SubscriptionTracker from 'components/utils/SubscriptionTracker';
   import huePubSub from 'utils/huePubSub';

+ 1 - 1
desktop/core/src/desktop/js/apps/editor/components/ExecutionStatusIcon.vue

@@ -25,7 +25,7 @@
 <script lang="ts">
   import { defineComponent, PropType, toRefs, computed } from 'vue';
 
-  import { ExecutionStatus } from '../execution/executable';
+  import { ExecutionStatus } from '../execution/sqlExecutable';
   import I18n from 'utils/i18n';
 
   interface StatusSpec {

+ 1 - 1
desktop/core/src/desktop/js/apps/editor/components/QueryHistoryTable.vue

@@ -91,7 +91,7 @@
   import ExecutionStatusIcon from './ExecutionStatusIcon.vue';
   import './QueryHistoryTable.scss';
   import { fetchHistory, FetchHistoryResponse } from '../api';
-  import { ExecutionStatus } from '../execution/executable';
+  import { ExecutionStatus } from '../execution/sqlExecutable';
   import { CancellablePromise } from 'api/cancellablePromise';
   import {
     EXECUTABLE_TRANSITIONED_TOPIC,

+ 2 - 2
desktop/core/src/desktop/js/apps/editor/components/events.ts

@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import Executable from '../execution/executable';
+import SqlExecutable from '../execution/sqlExecutable';
 
 export const EXECUTE_ACTIVE_EXECUTABLE_TOPIC = 'execute.active.executable';
-export type ExecuteActiveExecutableEvent = Executable;
+export type ExecuteActiveExecutableEvent = SqlExecutable;

+ 3 - 4
desktop/core/src/desktop/js/apps/editor/components/executionAnalysis/ExecutionAnalysisPanel.vue

@@ -56,8 +56,7 @@
   import { defineComponent, computed, onBeforeUnmount, ref, reactive } from 'vue';
 
   import { ExecutionJob } from 'apps/editor/execution/api';
-  import Executable, { ExecutionStatus } from 'apps/editor/execution/executable';
-  import SqlExecutable from 'apps/editor/execution/sqlExecutable';
+  import SqlExecutable, { ExecutionStatus } from 'apps/editor/execution/sqlExecutable';
   import { ExecutionError } from 'apps/editor/execution/executionLogs';
   import HueLink from 'components/HueLink.vue';
   import LogsPanel from 'components/LogsPanel.vue';
@@ -88,7 +87,7 @@
       const jobsAvailable = computed(() => !!jobs.length);
       const jobsWithUrls = computed(() => jobs.filter(job => job.url));
 
-      const debouncedUpdate = debounce((executable: Executable): void => {
+      const debouncedUpdate = debounce((executable: SqlExecutable): void => {
         const { status, logs } = executable;
         executionLogs.value = logs.fullLog;
         jobs.splice(0, jobs.length, ...logs.jobs);
@@ -96,7 +95,7 @@
         analysisAvailable.value = status !== ExecutionStatus.ready || !!errors.length;
       }, 5);
 
-      const updateFromExecutable = (executable: Executable): void => {
+      const updateFromExecutable = (executable: SqlExecutable): void => {
         if (!props.executable || props.executable.id !== executable.id) {
           return;
         }

+ 3 - 3
desktop/core/src/desktop/js/apps/editor/components/presentationMode/PresentationModeKoBridge.vue

@@ -38,7 +38,7 @@
   import { wrap } from 'vue/webComponentWrap';
 
   import PresentationMode from './PresentationMode.vue';
-  import Executable from 'apps/editor/execution/executable';
+  import SqlExecutable from 'apps/editor/execution/sqlExecutable';
   import Executor from 'apps/editor/execution/executor';
   import SubscriptionTracker from 'components/utils/SubscriptionTracker';
 
@@ -92,9 +92,9 @@
       };
     },
     methods: {
-      onBeforeExecute(executable: Executable): void {
+      onBeforeExecute(executable: SqlExecutable): void {
         this.$el.dispatchEvent(
-          new CustomEvent<Executable>('before-execute', { bubbles: true, detail: executable })
+          new CustomEvent<SqlExecutable>('before-execute', { bubbles: true, detail: executable })
         );
       },
       onClose(): void {

+ 3 - 3
desktop/core/src/desktop/js/apps/editor/components/result/ResultTable.vue

@@ -60,7 +60,7 @@
     ExecutableResultUpdatedEvent,
     ExecutableUpdatedEvent
   } from 'apps/editor/execution/events';
-  import Executable, { ExecutionStatus } from 'apps/editor/execution/executable';
+  import SqlExecutable, { ExecutionStatus } from 'apps/editor/execution/sqlExecutable';
   import ExecutionResult, { ResultRow, ResultType } from 'apps/editor/execution/executionResult';
   import { Column } from 'components/HueTable';
   import HueTable from 'components/HueTable.vue';
@@ -75,7 +75,7 @@
     },
     props: {
       executable: {
-        type: Object as PropType<Executable | undefined>,
+        type: Object as PropType<SqlExecutable | undefined>,
         default: undefined
       }
     },
@@ -136,7 +136,7 @@
         columns.value = [];
       };
 
-      const updateFromExecutable = (executable: Executable): void => {
+      const updateFromExecutable = (executable: SqlExecutable): void => {
         status.value = executable.status;
         hasResultSet.value = !!(executable.handle && executable.handle.has_result_set);
         if (!hasResultSet.value) {

+ 1 - 1
desktop/core/src/desktop/js/apps/editor/components/resultChart/ko.resultChart.js

@@ -28,7 +28,7 @@ import {
 } from './chartTransformers';
 import { attachTracker } from 'apps/editor/components/executableStateHandler';
 import { REDRAW_CHART_EVENT } from 'apps/editor/events';
-import { ExecutionStatus } from 'apps/editor/execution/executable';
+import { ExecutionStatus } from 'apps/editor/execution/sqlExecutable';
 import { RESULT_TYPE } from 'apps/editor/execution/executionResult';
 import { CURRENT_QUERY_TAB_SWITCHED_EVENT } from 'apps/editor/snippet';
 import componentUtils from 'ko/components/componentUtils';

+ 1 - 1
desktop/core/src/desktop/js/apps/editor/components/resultGrid/ko.simpleResultGrid.js

@@ -32,7 +32,7 @@ import {
   SHOW_NORMAL_RESULT_EVENT
 } from 'apps/editor/events';
 import { trackResult } from 'apps/editor/components/executableStateHandler';
-import { ExecutionStatus } from 'apps/editor/execution/executable';
+import { ExecutionStatus } from 'apps/editor/execution/sqlExecutable';
 
 export const SIMPLE_RESULT_GRID_COMPONENT = 'simple-result-grid';
 

+ 1 - 2
desktop/core/src/desktop/js/apps/editor/components/sqlScratchpad/SqlScratchpad.vue

@@ -78,7 +78,6 @@
     ExecutableLogsUpdatedEvent,
     ExecutableTransitionedEvent
   } from '../../execution/events';
-  import { ExecutionStatus } from '../../execution/executable';
   import ExecutionLogs from '../../execution/executionLogs';
   import AceEditor from '../aceEditor/AceEditor.vue';
   import { ActiveStatementChangedEventDetails } from '../aceEditor/types';
@@ -88,7 +87,7 @@
   import ExecutionAnalysisPanel from '../executionAnalysis/ExecutionAnalysisPanel.vue';
   import ResultTable from '../result/ResultTable.vue';
   import Executor from '../../execution/executor';
-  import SqlExecutable from '../../execution/sqlExecutable';
+  import SqlExecutable, { ExecutionStatus } from '../../execution/sqlExecutable';
   import { login } from 'api/auth';
   import { setBaseUrl } from 'api/utils';
   import contextCatalog from 'catalog/contextCatalog';

+ 7 - 5
desktop/core/src/desktop/js/apps/editor/execution/api.ts

@@ -23,9 +23,11 @@ import {
   ExecutableTransitionedEvent
 } from './events';
 import Executor from './executor';
-import SqlExecutable from './sqlExecutable';
 import { DefaultApiResponse, extractErrorMessage, post, successResponseIsError } from 'api/utils';
-import Executable, { ExecutableContext, ExecutionStatus } from 'apps/editor/execution/executable';
+import SqlExecutable, {
+  ExecutableContext,
+  ExecutionStatus
+} from 'apps/editor/execution/sqlExecutable';
 import { ResultRow, ResultType } from 'apps/editor/execution/executionResult';
 import { CancellablePromise } from 'api/cancellablePromise';
 import SubscriptionTracker from 'components/utils/SubscriptionTracker';
@@ -143,7 +145,7 @@ export interface ExecuteStatusApiResponse {
 }
 
 export interface ExecuteApiOptions {
-  executable: Executable;
+  executable: SqlExecutable;
   silenceErrors?: boolean;
 }
 
@@ -410,7 +412,7 @@ export const checkExecutionStatus = async (
 };
 
 export const fetchResults = async (options: {
-  executable: Executable;
+  executable: SqlExecutable;
   rows: number;
   startOver?: boolean;
   silenceErrors?: boolean;
@@ -455,7 +457,7 @@ export const fetchResultSize = async (
 };
 
 export const fetchLogs = async (options: {
-  executable: Executable;
+  executable: SqlExecutable;
   silenceErrors?: boolean;
   fullLog: string;
   jobs?: ExecutionJob[];

+ 3 - 3
desktop/core/src/desktop/js/apps/editor/execution/events.ts

@@ -16,18 +16,18 @@
 
 import { Session } from 'apps/editor/execution/api';
 import ExecutionResult from './executionResult';
-import Executable, { ExecutionStatus } from './executable';
+import SqlExecutable, { ExecutionStatus } from './sqlExecutable';
 import ExecutionLogs from './executionLogs';
 
 export const EXECUTABLE_TRANSITIONED_TOPIC = 'hue.executable.status.transitioned';
 export interface ExecutableTransitionedEvent {
   newStatus: ExecutionStatus;
   oldStatus: ExecutionStatus;
-  executable: Executable;
+  executable: SqlExecutable;
 }
 
 export const EXECUTABLE_UPDATED_TOPIC = 'hue.executable.updated';
-export type ExecutableUpdatedEvent = Executable;
+export type ExecutableUpdatedEvent = SqlExecutable;
 
 export const EXECUTABLE_LOGS_UPDATED_TOPIC = 'hue.executable.logs.updated';
 export type ExecutableLogsUpdatedEvent = ExecutionLogs;

+ 0 - 493
desktop/core/src/desktop/js/apps/editor/execution/executable.ts

@@ -1,493 +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 {
-  EXECUTABLE_TRANSITIONED_TOPIC,
-  EXECUTABLE_UPDATED_TOPIC,
-  ExecutableTransitionedEvent,
-  ExecutableUpdatedEvent
-} from 'apps/editor/execution/events';
-import { Cancellable } from 'api/cancellablePromise';
-import {
-  checkExecutionStatus,
-  closeStatement,
-  ExecuteApiResponse,
-  ExecutionHandle,
-  ExecutionHistory
-} from 'apps/editor/execution/api';
-import ExecutionResult from 'apps/editor/execution/executionResult';
-import { hueWindow } from 'types/types';
-import hueAnalytics from 'utils/hueAnalytics';
-import huePubSub from 'utils/huePubSub';
-import sessionManager from 'apps/editor/execution/sessionManager';
-import ExecutionLogs, {
-  ExecutionError,
-  ExecutionLogsRaw
-} from 'apps/editor/execution/executionLogs';
-import UUID from 'utils/string/UUID';
-import Executor from 'apps/editor/execution/executor';
-
-/**
- *
- * @type {{running: string, canceling: string, canceled: string, expired: string, waiting: string, success: string, ready: string, available: string, closed: string, starting: string}}
- */
-export enum ExecutionStatus {
-  available = 'available',
-  failed = 'failed',
-  success = 'success',
-  expired = 'expired',
-  running = 'running',
-  starting = 'starting',
-  waiting = 'waiting',
-  ready = 'ready',
-  streaming = 'streaming',
-  canceled = 'canceled',
-  canceling = 'canceling',
-  closed = 'closed'
-}
-
-export interface ExecutableRaw {
-  executeEnded: number;
-  executeStarted: number;
-  handle?: ExecutionHandle;
-  history?: ExecutionHistory;
-  id: string;
-  logs: ExecutionLogsRaw;
-  lost: boolean;
-  observerState: { [key: string]: unknown };
-  progress: number;
-  status: ExecutionStatus;
-  type: string;
-}
-
-export default abstract class Executable {
-  id: string = UUID();
-  database?: string;
-  executor: Executor;
-  handle?: ExecutionHandle;
-  operationId?: string;
-  history?: ExecutionHistory;
-  status = ExecutionStatus.ready;
-  progress = 0;
-  result?: ExecutionResult;
-  logs: ExecutionLogs;
-  cancellables: Cancellable[] = [];
-  notifyThrottle = -1;
-  executeStarted = 0;
-  executeEnded = 0;
-  previousExecutable?: Executable;
-  nextExecutable?: Executable;
-  observerState: { [key: string]: unknown } = {};
-  lost = false;
-  edited = false;
-
-  protected constructor(options: { executor: Executor }) {
-    this.executor = options.executor;
-    this.logs = new ExecutionLogs(this);
-  }
-
-  getLogs(): ExecutionLogs | undefined {
-    return this.logs;
-  }
-
-  getResult(): ExecutionResult | undefined {
-    return this.result;
-  }
-
-  setStatus(status: ExecutionStatus): void {
-    const oldStatus = this.status;
-    this.status = status;
-    if (oldStatus !== status) {
-      huePubSub.publish<ExecutableTransitionedEvent>(EXECUTABLE_TRANSITIONED_TOPIC, {
-        executable: this,
-        oldStatus: oldStatus,
-        newStatus: status
-      });
-    }
-    this.notify();
-  }
-
-  setProgress(progress: number): void {
-    this.progress = progress;
-    this.notify();
-  }
-
-  getExecutionStatus(): ExecutionStatus {
-    return this.status;
-  }
-
-  getExecutionTime(): number {
-    return (this.executeEnded || Date.now()) - this.executeStarted;
-  }
-
-  notify(sync?: boolean): void {
-    window.clearTimeout(this.notifyThrottle);
-    if (sync) {
-      huePubSub.publish<ExecutableUpdatedEvent>(EXECUTABLE_UPDATED_TOPIC, this);
-    } else {
-      this.notifyThrottle = window.setTimeout(() => {
-        huePubSub.publish<ExecutableUpdatedEvent>(EXECUTABLE_UPDATED_TOPIC, this);
-      }, 1);
-    }
-  }
-
-  isReady(): boolean {
-    return (
-      this.status === ExecutionStatus.ready ||
-      this.status === ExecutionStatus.closed ||
-      this.status === ExecutionStatus.canceled
-    );
-  }
-
-  isRunning(): boolean {
-    return this.status === ExecutionStatus.running || this.status === ExecutionStatus.streaming;
-  }
-
-  isSuccess(): boolean {
-    return this.status === ExecutionStatus.success || this.status === ExecutionStatus.available;
-  }
-
-  isFailed(): boolean {
-    return this.status === ExecutionStatus.failed;
-  }
-
-  isPartOfRunningExecution(): boolean {
-    return (
-      !this.isReady() ||
-      (!!this.previousExecutable && this.previousExecutable.isPartOfRunningExecution())
-    );
-  }
-
-  async cancelBatchChain(wait?: boolean): Promise<void> {
-    if (this.previousExecutable) {
-      this.previousExecutable.nextExecutable = undefined;
-      const cancelPromise = this.previousExecutable.cancelBatchChain(wait);
-      if (wait) {
-        await cancelPromise;
-      }
-      this.previousExecutable = undefined;
-    }
-
-    if (!this.isReady()) {
-      if (wait) {
-        await this.cancel();
-      } else {
-        this.cancel();
-      }
-    }
-
-    if (this.nextExecutable) {
-      this.nextExecutable.previousExecutable = undefined;
-      const cancelPromise = this.nextExecutable.cancelBatchChain(wait);
-      if (wait) {
-        await cancelPromise;
-      }
-      this.nextExecutable = undefined;
-    }
-    this.notify();
-  }
-
-  async execute(): Promise<void> {
-    if (!this.isReady()) {
-      return;
-    }
-    this.edited = false;
-    this.executeStarted = Date.now();
-
-    this.setStatus(ExecutionStatus.running);
-    this.setProgress(0);
-    this.notify(true);
-
-    try {
-      hueAnalytics.log(
-        'notebook',
-        'execute/' + (this.executor.connector() ? this.executor.connector().dialect : '')
-      );
-      try {
-        const response = await this.internalExecute();
-        this.handle = response.handle;
-        this.history = response.history;
-        if (response.history) {
-          this.operationId = response.history.uuid;
-        }
-        if (
-          response.handle.session_id &&
-          response.handle.session_type &&
-          response.handle.session_guid
-        ) {
-          sessionManager.updateSession({
-            type: response.handle.session_type,
-            id: response.handle.session_id,
-            session_id: response.handle.session_guid,
-            properties: []
-          });
-        }
-      } catch (err) {
-        if (err && (err.message || typeof err === 'string')) {
-          const adapted = this.adaptError((err.message && err.message) || err);
-          this.logs.errors.push(adapted);
-          this.logs.notify();
-        }
-        throw err;
-      }
-
-      if (this.handle && this.handle.has_result_set && this.handle.sync) {
-        this.result = new ExecutionResult(this);
-        if (this.handle.sync) {
-          if (this.handle.result) {
-            this.result.handleResultResponse(this.handle.result);
-          }
-          this.result.fetchRows();
-        }
-      }
-
-      if (this.executor.isSqlAnalyzerEnabled && this.history) {
-        huePubSub.publish('editor.upload.query', this.history.id);
-      }
-
-      this.checkStatus();
-      this.logs.fetchLogs();
-    } catch (err) {
-      console.warn(err);
-      this.setStatus(ExecutionStatus.failed);
-    }
-  }
-
-  async checkStatus(statusCheckCount?: number): Promise<void> {
-    if (!this.handle) {
-      return;
-    }
-
-    let checkStatusTimeout = -1;
-
-    let actualCheckCount = statusCheckCount || 0;
-    if (!statusCheckCount) {
-      this.addCancellable({
-        cancel: () => {
-          window.clearTimeout(checkStatusTimeout);
-        }
-      });
-    }
-    actualCheckCount++;
-
-    const queryStatus = await checkExecutionStatus({ executable: this });
-
-    switch (queryStatus.status) {
-      case ExecutionStatus.success:
-        this.executeEnded = Date.now();
-        this.setStatus(queryStatus.status);
-        this.setProgress(99); // TODO: why 99 here (from old code)?
-        break;
-      case ExecutionStatus.available:
-        this.executeEnded = Date.now();
-        this.setStatus(queryStatus.status);
-        this.setProgress(100);
-        if (!this.result && this.handle && this.handle.has_result_set) {
-          this.result = new ExecutionResult(this);
-          this.result.fetchRows();
-        }
-        if (this.nextExecutable) {
-          if (!this.nextExecutable.isReady()) {
-            await this.nextExecutable.reset();
-          }
-          this.nextExecutable.execute();
-        }
-        break;
-      case ExecutionStatus.canceled:
-      case ExecutionStatus.expired:
-        this.executeEnded = Date.now();
-        this.setStatus(queryStatus.status);
-        break;
-      case ExecutionStatus.streaming:
-        if (!queryStatus.result) {
-          return;
-        }
-        if ((<hueWindow>window).WEB_SOCKETS_ENABLED) {
-          huePubSub.publish('editor.ws.query.fetch_result', queryStatus.result);
-        } else {
-          if (!this.result) {
-            this.result = new ExecutionResult(this, true);
-          }
-          this.result.handleResultResponse(queryStatus.result);
-        }
-      case ExecutionStatus.running:
-      case ExecutionStatus.starting:
-      case ExecutionStatus.waiting:
-        this.setStatus(queryStatus.status);
-        checkStatusTimeout = window.setTimeout(
-          () => {
-            this.checkStatus(statusCheckCount);
-          },
-          actualCheckCount > 45 ? 5000 : 1000
-        );
-        break;
-      case ExecutionStatus.failed:
-        this.executeEnded = Date.now();
-        this.setStatus(queryStatus.status);
-        if (queryStatus.message) {
-          huePubSub.publish('hue.error', queryStatus.message);
-        }
-        break;
-      default:
-        this.executeEnded = Date.now();
-        this.setStatus(ExecutionStatus.failed);
-        console.warn('Got unknown status ' + queryStatus.status);
-    }
-  }
-
-  addCancellable(cancellable: Cancellable): void {
-    this.cancellables.push(cancellable);
-  }
-
-  abstract async internalExecute(): Promise<ExecuteApiResponse>;
-
-  abstract adaptError(err: string): ExecutionError;
-
-  abstract canExecuteInBatch(): boolean;
-
-  abstract getKey(): string;
-
-  abstract getRawStatement(): string;
-
-  abstract getStatement(): string;
-
-  abstract toJson(): string;
-
-  async cancel(): Promise<void> {
-    if (
-      this.cancellables.length &&
-      (this.status === ExecutionStatus.running || this.status === ExecutionStatus.streaming)
-    ) {
-      hueAnalytics.log(
-        'notebook',
-        'cancel/' + (this.executor.connector() ? this.executor.connector().dialect : '')
-      );
-      this.setStatus(ExecutionStatus.canceling);
-      while (this.cancellables.length) {
-        const cancellable = this.cancellables.pop();
-        if (cancellable) {
-          await cancellable.cancel();
-        }
-      }
-      this.setStatus(ExecutionStatus.canceled);
-    }
-  }
-
-  async reset(): Promise<void> {
-    this.result = undefined;
-    this.logs.reset();
-    if (!this.isReady()) {
-      try {
-        await this.close();
-      } catch (err) {}
-    }
-    this.handle = undefined;
-    this.setProgress(0);
-    this.setStatus(ExecutionStatus.ready);
-  }
-
-  toJs(): ExecutableRaw {
-    const state = Object.assign({}, this.observerState);
-    delete state.aceAnchor;
-
-    return {
-      executeEnded: this.executeEnded,
-      executeStarted: this.executeStarted,
-      handle: this.handle,
-      history: this.history,
-      id: this.id,
-      logs: this.logs.toJs(),
-      lost: this.lost,
-      observerState: state,
-      progress: this.progress,
-      status: this.status,
-      type: 'executable'
-    };
-  }
-
-  async close(): Promise<void> {
-    while (this.cancellables.length) {
-      const nextCancellable = this.cancellables.pop();
-      if (nextCancellable) {
-        try {
-          await nextCancellable.cancel();
-        } catch (err) {
-          console.warn(err);
-        }
-      }
-    }
-
-    try {
-      await closeStatement({ executable: this, silenceErrors: true });
-    } catch (err) {
-      console.warn('Failed closing statement');
-    }
-    this.setStatus(ExecutionStatus.closed);
-  }
-
-  async toContext(id?: string): Promise<ExecutableContext> {
-    const session = await sessionManager.getSession({ type: this.executor.connector().id });
-    if (this.executor.snippet) {
-      return {
-        operationId: this.operationId,
-        snippet: this.executor.snippet.toContextJson(this.getStatement()),
-        notebook: JSON.stringify(await this.executor.snippet.parentNotebook.toJs())
-      };
-    }
-
-    const snippet = {
-      type: this.executor.connector().id,
-      result: {
-        handle: this.handle
-      },
-      connector: this.executor.connector(),
-      executor: this.executor.toJs(),
-      defaultLimit: (this.executor.defaultLimit && this.executor.defaultLimit()) || null,
-      status: this.status,
-      id: id || UUID(),
-      statement_raw: this.getRawStatement(),
-      statement: this.getStatement(),
-      lastExecuted: this.executeStarted,
-      variables: [],
-      compute: this.executor.compute(),
-      namespace: this.executor.namespace(),
-      database: this.database,
-      properties: { settings: [] }
-    };
-
-    const notebook = {
-      type: `query-${this.executor.connector().id}`,
-      snippets: [snippet],
-      uuid: UUID(),
-      name: '',
-      isSaved: false,
-      sessions: [session],
-      editorWsChannel: (<hueWindow>window).WS_CHANNEL
-    };
-
-    return {
-      operationId: this.operationId,
-      snippet: JSON.stringify(snippet),
-      notebook: JSON.stringify(notebook)
-    };
-  }
-}
-
-export interface ExecutableContext {
-  operationId?: string;
-  snippet: string;
-  notebook: string;
-}

+ 3 - 3
desktop/core/src/desktop/js/apps/editor/execution/executionLogs.ts

@@ -20,7 +20,7 @@ import {
   ExecutableLogsUpdatedEvent
 } from 'apps/editor/execution/events';
 import huePubSub from 'utils/huePubSub';
-import Executable, { ExecutionStatus } from './executable';
+import SqlExecutable, { ExecutionStatus } from './sqlExecutable';
 
 export interface ExecutionError {
   row: number;
@@ -34,13 +34,13 @@ export interface ExecutionLogsRaw {
 }
 
 export default class ExecutionLogs {
-  executable: Executable;
+  executable: SqlExecutable;
   fullLog = '';
   logLines = 0;
   jobs: ExecutionJob[] = [];
   errors: ExecutionError[] = [];
 
-  constructor(executable: Executable) {
+  constructor(executable: SqlExecutable) {
     this.executable = executable;
   }
 

+ 3 - 3
desktop/core/src/desktop/js/apps/editor/execution/executionResult.ts

@@ -30,7 +30,7 @@ import * as ko from 'knockout';
 
 import huePubSub from 'utils/huePubSub';
 import sleep from 'utils/timing/sleep';
-import Executable, { ExecutionStatus } from './executable';
+import SqlExecutable, { ExecutionStatus } from './sqlExecutable';
 
 export const RESULT_TYPE = {
   TABLE: 'table'
@@ -96,7 +96,7 @@ export enum ResultType {
 }
 
 export default class ExecutionResult {
-  executable: Executable;
+  executable: SqlExecutable;
   streaming: boolean;
 
   type?: ResultType;
@@ -115,7 +115,7 @@ export default class ExecutionResult {
   isEscaped = false;
   fetchedOnce = false;
 
-  constructor(executable: Executable, streaming?: boolean) {
+  constructor(executable: SqlExecutable, streaming?: boolean) {
     this.executable = executable;
     this.streaming = !!streaming;
   }

+ 6 - 6
desktop/core/src/desktop/js/apps/editor/execution/executor.ts

@@ -17,7 +17,7 @@
 import KnockoutObservable from '@types/knockout';
 
 import Snippet from 'apps/editor/snippet';
-import Executable, { ExecutableRaw } from 'apps/editor/execution/executable';
+import SqlExecutable, { ExecutableRaw } from 'apps/editor/execution/sqlExecutable';
 import { syncSqlExecutables } from 'apps/editor/execution/utils';
 import { StatementDetails } from 'parse/types';
 import { Compute, Connector, Namespace } from 'config/types';
@@ -46,9 +46,9 @@ export default class Executor {
   defaultLimit?: KnockoutObservable<number>;
   isSqlEngine?: boolean;
   isSqlAnalyzerEnabled?: boolean;
-  executables: Executable[] = [];
+  executables: SqlExecutable[] = [];
   snippet?: Snippet;
-  activeExecutable?: Executable;
+  activeExecutable?: SqlExecutable;
   variables: VariableIndex = {};
 
   constructor(options: ExecutorOptions) {
@@ -73,13 +73,13 @@ export default class Executor {
     this.executables.forEach(existingExecutable => existingExecutable.cancelBatchChain());
   }
 
-  setExecutables(executables: Executable[]): void {
+  setExecutables(executables: SqlExecutable[]): void {
     this.cancelAll();
     this.executables = executables;
     this.executables.forEach(executable => executable.notify());
   }
 
-  update(statementDetails: StatementDetails, beforeExecute: boolean): Executable {
+  update(statementDetails: StatementDetails, beforeExecute: boolean): SqlExecutable {
     const executables = syncSqlExecutables(this, statementDetails);
 
     // Cancel any "lost" executables and any batch chain it's part of
@@ -92,7 +92,7 @@ export default class Executor {
     if (beforeExecute) {
       executables.selected.forEach(executable => executable.cancelBatchChain());
 
-      let previous: Executable | undefined;
+      let previous: SqlExecutable | undefined;
       executables.selected.forEach(executable => {
         if (previous) {
           executable.previousExecutable = previous;

+ 1 - 1
desktop/core/src/desktop/js/apps/editor/execution/sqlExecutable.test.ts

@@ -18,7 +18,7 @@ import { CancellablePromise } from 'api/cancellablePromise';
 
 import Executor from 'apps/editor/execution/executor';
 import SqlExecutable from './sqlExecutable';
-import { ExecutionStatus } from './executable';
+import { ExecutionStatus } from './sqlExecutable';
 import sessionManager from './sessionManager';
 import * as ApiUtils from 'api/utils';
 import { ParsedSqlStatement } from 'parse/sqlStatementsParser';

+ 496 - 55
desktop/core/src/desktop/js/apps/editor/execution/sqlExecutable.ts

@@ -14,13 +14,69 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import dataCatalog from 'catalog/dataCatalog';
-import { ExecuteApiResponse, executeStatement } from 'apps/editor/execution/api';
-import Executable, { ExecutableRaw } from 'apps/editor/execution/executable';
-import { ExecutionError } from 'apps/editor/execution/executionLogs';
+import { VariableIndex } from '../components/variableSubstitution/types';
+import { Cancellable } from 'api/cancellablePromise';
+import {
+  checkExecutionStatus,
+  closeStatement,
+  ExecuteApiResponse,
+  executeStatement,
+  ExecutionHandle,
+  ExecutionHistory
+} from 'apps/editor/execution/api';
+import {
+  EXECUTABLE_TRANSITIONED_TOPIC,
+  EXECUTABLE_UPDATED_TOPIC,
+  ExecutableTransitionedEvent,
+  ExecutableUpdatedEvent
+} from 'apps/editor/execution/events';
+import ExecutionResult from 'apps/editor/execution/executionResult';
+import ExecutionLogs, {
+  ExecutionError,
+  ExecutionLogsRaw
+} from 'apps/editor/execution/executionLogs';
 import Executor from 'apps/editor/execution/executor';
+import sessionManager from 'apps/editor/execution/sessionManager';
+import dataCatalog from 'catalog/dataCatalog';
 import { ParsedSqlStatement } from 'parse/sqlStatementsParser';
-import { VariableIndex } from '../components/variableSubstitution/types';
+import { hueWindow } from 'types/types';
+import hueAnalytics from 'utils/hueAnalytics';
+import huePubSub from 'utils/huePubSub';
+import UUID from 'utils/string/UUID';
+
+export enum ExecutionStatus {
+  available = 'available',
+  failed = 'failed',
+  success = 'success',
+  expired = 'expired',
+  running = 'running',
+  starting = 'starting',
+  waiting = 'waiting',
+  ready = 'ready',
+  streaming = 'streaming',
+  canceled = 'canceled',
+  canceling = 'canceling',
+  closed = 'closed'
+}
+
+export interface ExecutableRaw {
+  executeEnded: number;
+  executeStarted: number;
+  handle?: ExecutionHandle;
+  history?: ExecutionHistory;
+  id: string;
+  logs: ExecutionLogsRaw;
+  lost: boolean;
+  observerState: { [key: string]: unknown };
+  progress: number;
+  status: ExecutionStatus;
+  type: string;
+}
+
+export interface SqlExecutableRaw extends ExecutableRaw {
+  database: string;
+  parsedStatement: ParsedSqlStatement;
+}
 
 const BATCHABLE_STATEMENT_TYPES =
   /ALTER|ANALYZE|WITH|REFRESH|CREATE|DELETE|DROP|GRANT|INSERT|INVALIDATE|LOAD|SET|TRUNCATE|UPDATE|UPSERT|USE/i;
@@ -31,11 +87,6 @@ const TABLE_DDL_REGEX =
 const DB_DDL_REGEX =
   /(?:CREATE|DROP)\s+(?:DATABASE|SCHEMA)\s+(?:IF\s+(?:NOT\s+)?EXISTS\s+)?(?:`([^`]+)`|([^;\s]+))/i;
 
-export interface SqlExecutableRaw extends ExecutableRaw {
-  database: string;
-  parsedStatement: ParsedSqlStatement;
-}
-
 const substituteVariables = (statement: string, variables: VariableIndex): string => {
   if (!Object.keys(variables).length) {
     return statement;
@@ -57,8 +108,26 @@ const substituteVariables = (statement: string, variables: VariableIndex): strin
   );
 };
 
-export default class SqlExecutable extends Executable {
+export default class SqlExecutable {
+  id: string = UUID();
   database: string;
+  executor: Executor;
+  handle?: ExecutionHandle;
+  operationId?: string;
+  history?: ExecutionHistory;
+  status = ExecutionStatus.ready;
+  progress = 0;
+  result?: ExecutionResult;
+  logs: ExecutionLogs;
+  cancellables: Cancellable[] = [];
+  notifyThrottle = -1;
+  executeStarted = 0;
+  executeEnded = 0;
+  previousExecutable?: SqlExecutable;
+  nextExecutable?: SqlExecutable;
+  observerState: { [key: string]: unknown } = {};
+  lost = false;
+  edited = false;
   parsedStatement: ParsedSqlStatement;
 
   constructor(options: {
@@ -66,41 +135,263 @@ export default class SqlExecutable extends Executable {
     database: string;
     parsedStatement: ParsedSqlStatement;
   }) {
-    super(options);
+    this.logs = new ExecutionLogs(this);
+    this.executor = options.executor;
     this.database = options.database;
     this.parsedStatement = options.parsedStatement;
   }
 
-  getRawStatement(): string {
-    return this.parsedStatement.statement;
+  getLogs(): ExecutionLogs | undefined {
+    return this.logs;
   }
 
-  getStatement(): string {
-    let statement = this.getRawStatement();
+  getResult(): ExecutionResult | undefined {
+    return this.result;
+  }
 
-    if (
-      this.parsedStatement.firstToken &&
-      this.parsedStatement.firstToken.toLowerCase() === 'select' &&
-      this.executor.defaultLimit &&
-      !isNaN(this.executor.defaultLimit()) &&
-      this.executor.defaultLimit() > 0 &&
-      /\sfrom\s/i.test(statement) &&
-      !/\slimit\s[0-9]/i.test(statement)
-    ) {
-      const endMatch = statement.match(SELECT_END_REGEX);
-      if (endMatch) {
-        statement = endMatch[1] + ' LIMIT ' + this.executor.defaultLimit();
-        if (endMatch[2]) {
-          statement += endMatch[2];
+  setStatus(status: ExecutionStatus): void {
+    const oldStatus = this.status;
+    this.status = status;
+    if (oldStatus !== status) {
+      huePubSub.publish<ExecutableTransitionedEvent>(EXECUTABLE_TRANSITIONED_TOPIC, {
+        executable: this,
+        oldStatus: oldStatus,
+        newStatus: status
+      });
+    }
+    this.notify();
+  }
+
+  setProgress(progress: number): void {
+    this.progress = progress;
+    this.notify();
+  }
+
+  getExecutionStatus(): ExecutionStatus {
+    return this.status;
+  }
+
+  getExecutionTime(): number {
+    return (this.executeEnded || Date.now()) - this.executeStarted;
+  }
+
+  notify(sync?: boolean): void {
+    window.clearTimeout(this.notifyThrottle);
+    if (sync) {
+      huePubSub.publish<ExecutableUpdatedEvent>(EXECUTABLE_UPDATED_TOPIC, this);
+    } else {
+      this.notifyThrottle = window.setTimeout(() => {
+        huePubSub.publish<ExecutableUpdatedEvent>(EXECUTABLE_UPDATED_TOPIC, this);
+      }, 1);
+    }
+  }
+
+  isReady(): boolean {
+    return (
+      this.status === ExecutionStatus.ready ||
+      this.status === ExecutionStatus.closed ||
+      this.status === ExecutionStatus.canceled
+    );
+  }
+
+  isRunning(): boolean {
+    return this.status === ExecutionStatus.running || this.status === ExecutionStatus.streaming;
+  }
+
+  isSuccess(): boolean {
+    return this.status === ExecutionStatus.success || this.status === ExecutionStatus.available;
+  }
+
+  isFailed(): boolean {
+    return this.status === ExecutionStatus.failed;
+  }
+
+  isPartOfRunningExecution(): boolean {
+    return (
+      !this.isReady() ||
+      (!!this.previousExecutable && this.previousExecutable.isPartOfRunningExecution())
+    );
+  }
+
+  async cancelBatchChain(wait?: boolean): Promise<void> {
+    if (this.previousExecutable) {
+      this.previousExecutable.nextExecutable = undefined;
+      const cancelPromise = this.previousExecutable.cancelBatchChain(wait);
+      if (wait) {
+        await cancelPromise;
+      }
+      this.previousExecutable = undefined;
+    }
+
+    if (!this.isReady()) {
+      if (wait) {
+        await this.cancel();
+      } else {
+        this.cancel();
+      }
+    }
+
+    if (this.nextExecutable) {
+      this.nextExecutable.previousExecutable = undefined;
+      const cancelPromise = this.nextExecutable.cancelBatchChain(wait);
+      if (wait) {
+        await cancelPromise;
+      }
+      this.nextExecutable = undefined;
+    }
+    this.notify();
+  }
+
+  async execute(): Promise<void> {
+    if (!this.isReady()) {
+      return;
+    }
+    this.edited = false;
+    this.executeStarted = Date.now();
+
+    this.setStatus(ExecutionStatus.running);
+    this.setProgress(0);
+    this.notify(true);
+
+    try {
+      hueAnalytics.log(
+        'notebook',
+        'execute/' + (this.executor.connector() ? this.executor.connector().dialect : '')
+      );
+      try {
+        const response = await this.internalExecute();
+        this.handle = response.handle;
+        this.history = response.history;
+        if (response.history) {
+          this.operationId = response.history.uuid;
+        }
+        if (
+          response.handle.session_id &&
+          response.handle.session_type &&
+          response.handle.session_guid
+        ) {
+          sessionManager.updateSession({
+            type: response.handle.session_type,
+            id: response.handle.session_id,
+            session_id: response.handle.session_guid,
+            properties: []
+          });
+        }
+      } catch (err) {
+        if (err && (err.message || typeof err === 'string')) {
+          const adapted = this.adaptError((err.message && err.message) || err);
+          this.logs.errors.push(adapted);
+          this.logs.notify();
+        }
+        throw err;
+      }
+
+      if (this.handle && this.handle.has_result_set && this.handle.sync) {
+        this.result = new ExecutionResult(this);
+        if (this.handle.sync) {
+          if (this.handle.result) {
+            this.result.handleResultResponse(this.handle.result);
+          }
+          this.result.fetchRows();
         }
       }
+
+      if (this.executor.isSqlAnalyzerEnabled && this.history) {
+        huePubSub.publish('editor.upload.query', this.history.id);
+      }
+
+      this.checkStatus();
+      this.logs.fetchLogs();
+    } catch (err) {
+      console.warn(err);
+      this.setStatus(ExecutionStatus.failed);
     }
+  }
 
-    if (this.executor.variables) {
-      statement = substituteVariables(statement, this.executor.variables);
+  async checkStatus(statusCheckCount?: number): Promise<void> {
+    if (!this.handle) {
+      return;
     }
 
-    return statement;
+    let checkStatusTimeout = -1;
+
+    let actualCheckCount = statusCheckCount || 0;
+    if (!statusCheckCount) {
+      this.addCancellable({
+        cancel: () => {
+          window.clearTimeout(checkStatusTimeout);
+        }
+      });
+    }
+    actualCheckCount++;
+
+    const queryStatus = await checkExecutionStatus({ executable: this });
+
+    switch (queryStatus.status) {
+      case ExecutionStatus.success:
+        this.executeEnded = Date.now();
+        this.setStatus(queryStatus.status);
+        this.setProgress(99); // TODO: why 99 here (from old code)?
+        break;
+      case ExecutionStatus.available:
+        this.executeEnded = Date.now();
+        this.setStatus(queryStatus.status);
+        this.setProgress(100);
+        if (!this.result && this.handle && this.handle.has_result_set) {
+          this.result = new ExecutionResult(this);
+          this.result.fetchRows();
+        }
+        if (this.nextExecutable) {
+          if (!this.nextExecutable.isReady()) {
+            await this.nextExecutable.reset();
+          }
+          this.nextExecutable.execute();
+        }
+        break;
+      case ExecutionStatus.canceled:
+      case ExecutionStatus.expired:
+        this.executeEnded = Date.now();
+        this.setStatus(queryStatus.status);
+        break;
+      case ExecutionStatus.streaming:
+        if (!queryStatus.result) {
+          return;
+        }
+        if ((<hueWindow>window).WEB_SOCKETS_ENABLED) {
+          huePubSub.publish('editor.ws.query.fetch_result', queryStatus.result);
+        } else {
+          if (!this.result) {
+            this.result = new ExecutionResult(this, true);
+          }
+          this.result.handleResultResponse(queryStatus.result);
+        }
+      case ExecutionStatus.running:
+      case ExecutionStatus.starting:
+      case ExecutionStatus.waiting:
+        this.setStatus(queryStatus.status);
+        checkStatusTimeout = window.setTimeout(
+          () => {
+            this.checkStatus(statusCheckCount);
+          },
+          actualCheckCount > 45 ? 5000 : 1000
+        );
+        break;
+      case ExecutionStatus.failed:
+        this.executeEnded = Date.now();
+        this.setStatus(queryStatus.status);
+        if (queryStatus.message) {
+          huePubSub.publish('hue.error', queryStatus.message);
+        }
+        break;
+      default:
+        this.executeEnded = Date.now();
+        this.setStatus(ExecutionStatus.failed);
+        console.warn('Got unknown status ' + queryStatus.status);
+    }
+  }
+
+  addCancellable(cancellable: Cancellable): void {
+    this.cancellables.push(cancellable);
   }
 
   async internalExecute(): Promise<ExecuteApiResponse> {
@@ -139,14 +430,99 @@ export default class SqlExecutable extends Executable {
     });
   }
 
-  getKey(): string {
-    return this.database + '_' + this.parsedStatement.statement;
+  adaptError(message: string): ExecutionError {
+    const match = ERROR_REGEX.exec(message);
+    if (match) {
+      const row = parseInt(match[1]);
+      const column = (match[3] && parseInt(match[3])) || 0;
+
+      return { message, column: column || 0, row };
+    }
+    return { message, column: 0, row: this.parsedStatement.location.first_line };
   }
 
   canExecuteInBatch(): boolean {
     return this.parsedStatement && BATCHABLE_STATEMENT_TYPES.test(this.parsedStatement.firstToken);
   }
 
+  getKey(): string {
+    return this.database + '_' + this.parsedStatement.statement;
+  }
+
+  getRawStatement(): string {
+    return this.parsedStatement.statement;
+  }
+
+  getStatement(): string {
+    let statement = this.getRawStatement();
+
+    if (
+      this.parsedStatement.firstToken &&
+      this.parsedStatement.firstToken.toLowerCase() === 'select' &&
+      this.executor.defaultLimit &&
+      !isNaN(this.executor.defaultLimit()) &&
+      this.executor.defaultLimit() > 0 &&
+      /\sfrom\s/i.test(statement) &&
+      !/\slimit\s[0-9]/i.test(statement)
+    ) {
+      const endMatch = statement.match(SELECT_END_REGEX);
+      if (endMatch) {
+        statement = endMatch[1] + ' LIMIT ' + this.executor.defaultLimit();
+        if (endMatch[2]) {
+          statement += endMatch[2];
+        }
+      }
+    }
+
+    if (this.executor.variables) {
+      statement = substituteVariables(statement, this.executor.variables);
+    }
+
+    return statement;
+  }
+
+  toJson(): string {
+    return JSON.stringify({
+      id: this.id,
+      parsedStatement: this.parsedStatement,
+      statement: this.getStatement(),
+      database: this.database
+    });
+  }
+
+  async cancel(): Promise<void> {
+    if (
+      this.cancellables.length &&
+      (this.status === ExecutionStatus.running || this.status === ExecutionStatus.streaming)
+    ) {
+      hueAnalytics.log(
+        'notebook',
+        'cancel/' + (this.executor.connector() ? this.executor.connector().dialect : '')
+      );
+      this.setStatus(ExecutionStatus.canceling);
+      while (this.cancellables.length) {
+        const cancellable = this.cancellables.pop();
+        if (cancellable) {
+          await cancellable.cancel();
+        }
+      }
+      this.setStatus(ExecutionStatus.canceled);
+    }
+  }
+
+  async reset(): Promise<void> {
+    this.result = undefined;
+    this.logs.reset();
+    if (!this.isReady()) {
+      try {
+        await this.close();
+      } catch (err) {}
+    }
+    this.handle = undefined;
+    this.setProgress(0);
+    this.setStatus(ExecutionStatus.ready);
+  }
+
   static fromJs(executor: Executor, executableRaw: SqlExecutableRaw): SqlExecutable {
     const executable = new SqlExecutable({
       database: executableRaw.database,
@@ -171,30 +547,95 @@ export default class SqlExecutable extends Executable {
   }
 
   toJs(): SqlExecutableRaw {
-    const executableJs = super.toJs() as unknown as SqlExecutableRaw;
-    executableJs.database = this.database;
-    executableJs.parsedStatement = this.parsedStatement;
-    executableJs.type = 'sqlExecutable';
-    return executableJs;
-  }
-
-  toJson(): string {
-    return JSON.stringify({
+    const state = Object.assign({}, this.observerState);
+    delete state.aceAnchor;
+    return {
+      executeEnded: this.executeEnded,
+      executeStarted: this.executeStarted,
+      handle: this.handle,
+      history: this.history,
       id: this.id,
-      parsedStatement: this.parsedStatement,
-      statement: this.getStatement(),
-      database: this.database
-    });
+      logs: this.logs.toJs(),
+      lost: this.lost,
+      observerState: state,
+      progress: this.progress,
+      status: this.status,
+      type: 'sqlExecutable',
+      database: this.database,
+      parsedStatement: this.parsedStatement
+    };
   }
 
-  adaptError(message: string): ExecutionError {
-    const match = ERROR_REGEX.exec(message);
-    if (match) {
-      const row = parseInt(match[1]);
-      const column = (match[3] && parseInt(match[3])) || 0;
+  async close(): Promise<void> {
+    while (this.cancellables.length) {
+      const nextCancellable = this.cancellables.pop();
+      if (nextCancellable) {
+        try {
+          await nextCancellable.cancel();
+        } catch (err) {
+          console.warn(err);
+        }
+      }
+    }
 
-      return { message, column: column || 0, row };
+    try {
+      await closeStatement({ executable: this, silenceErrors: true });
+    } catch (err) {
+      console.warn('Failed closing statement');
     }
-    return { message, column: 0, row: this.parsedStatement.location.first_line };
+    this.setStatus(ExecutionStatus.closed);
   }
+
+  async toContext(id?: string): Promise<ExecutableContext> {
+    const session = await sessionManager.getSession({ type: this.executor.connector().id });
+    if (this.executor.snippet) {
+      return {
+        operationId: this.operationId,
+        snippet: this.executor.snippet.toContextJson(this.getStatement()),
+        notebook: JSON.stringify(await this.executor.snippet.parentNotebook.toJs())
+      };
+    }
+
+    const snippet = {
+      type: this.executor.connector().id,
+      result: {
+        handle: this.handle
+      },
+      connector: this.executor.connector(),
+      executor: this.executor.toJs(),
+      defaultLimit: (this.executor.defaultLimit && this.executor.defaultLimit()) || null,
+      status: this.status,
+      id: id || UUID(),
+      statement_raw: this.getRawStatement(),
+      statement: this.getStatement(),
+      lastExecuted: this.executeStarted,
+      variables: [],
+      compute: this.executor.compute(),
+      namespace: this.executor.namespace(),
+      database: this.database,
+      properties: { settings: [] }
+    };
+
+    const notebook = {
+      type: `query-${this.executor.connector().id}`,
+      snippets: [snippet],
+      uuid: UUID(),
+      name: '',
+      isSaved: false,
+      sessions: [session],
+      editorWsChannel: (<hueWindow>window).WS_CHANNEL
+    };
+
+    return {
+      operationId: this.operationId,
+      snippet: JSON.stringify(snippet),
+      notebook: JSON.stringify(notebook)
+    };
+  }
+}
+
+export interface ExecutableContext {
+  operationId?: string;
+  snippet: string;
+  notebook: string;
 }

+ 1 - 2
desktop/core/src/desktop/js/apps/editor/execution/utils.ts

@@ -14,7 +14,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import Executable from 'apps/editor/execution/executable';
 import Executor from 'apps/editor/execution/executor';
 import SqlExecutable from 'apps/editor/execution/sqlExecutable';
 import { StatementDetails } from 'parse/types';
@@ -37,7 +36,7 @@ export const syncSqlExecutables = (
     ...statementDetails.followingStatements
   ];
 
-  const existingExecutables: (Executable | undefined)[] = [...executor.executables];
+  const existingExecutables: (SqlExecutable | undefined)[] = [...executor.executables];
 
   const result = {
     all: <SqlExecutable[]>[],

+ 1 - 2
desktop/core/src/desktop/js/apps/editor/snippet.js

@@ -43,9 +43,8 @@ import defer from 'utils/timing/defer';
 import UUID from 'utils/string/UUID';
 import { getFromLocalStorage, setInLocalStorage } from 'utils/storageUtils';
 import sessionManager from 'apps/editor/execution/sessionManager';
-import SqlExecutable from 'apps/editor/execution/sqlExecutable';
+import SqlExecutable, { ExecutionStatus } from 'apps/editor/execution/sqlExecutable';
 import { HIDE_FIXED_HEADERS_EVENT, REDRAW_FIXED_HEADERS_EVENT } from 'apps/editor/events';
-import { ExecutionStatus } from 'apps/editor/execution/executable';
 import {
   ACTIVE_STATEMENT_CHANGED_EVENT,
   REFRESH_STATEMENT_LOCATIONS_EVENT