Browse Source

[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 năm trước cách đây
mục cha
commit
4e395545a2
24 tập tin đã thay đổi với 550 bổ sung606 xóa
  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.
 // limitations under the License.
 
 
 import { format } from '@gethue/sql-formatter';
 import { format } from '@gethue/sql-formatter';
-import { ExecutionStatus } from './execution/executable';
+import { ExecutionStatus } from './execution/sqlExecutable';
 import { CancellablePromise } from 'api/cancellablePromise';
 import { CancellablePromise } from 'api/cancellablePromise';
 import { get } from 'api/utils';
 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 { EXECUTABLE_UPDATED_TOPIC, ExecutableUpdatedEvent } from 'apps/editor/execution/events';
 import { nextTick } from 'vue';
 import { nextTick } from 'vue';
 import { shallowMount, mount } from '@vue/test-utils';
 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 ExecutableProgressBar from './ExecutableProgressBar.vue';
 import huePubSub from 'utils/huePubSub';
 import huePubSub from 'utils/huePubSub';
 
 
@@ -36,7 +36,7 @@ describe('ExecutableProgressBar.vue', () => {
 
 
     const { element } = mount(ExecutableProgressBar, {
     const { element } = mount(ExecutableProgressBar, {
       propsData: {
       propsData: {
-        executable: <Executable>mockExecutable
+        executable: <SqlExecutable>mockExecutable
       }
       }
     });
     });
 
 
@@ -49,7 +49,7 @@ describe('ExecutableProgressBar.vue', () => {
     mockExecutable.progress = 10;
     mockExecutable.progress = 10;
     huePubSub.publish<ExecutableUpdatedEvent>(
     huePubSub.publish<ExecutableUpdatedEvent>(
       EXECUTABLE_UPDATED_TOPIC,
       EXECUTABLE_UPDATED_TOPIC,
-      mockExecutable as Executable
+      mockExecutable as SqlExecutable
     );
     );
     await nextTick();
     await nextTick();
 
 
@@ -65,7 +65,7 @@ describe('ExecutableProgressBar.vue', () => {
 
 
     const { element } = mount(ExecutableProgressBar, {
     const { element } = mount(ExecutableProgressBar, {
       propsData: {
       propsData: {
-        executable: <Executable>mockExecutable
+        executable: <SqlExecutable>mockExecutable
       }
       }
     });
     });
 
 
@@ -78,7 +78,7 @@ describe('ExecutableProgressBar.vue', () => {
     mockExecutable.progress = 10;
     mockExecutable.progress = 10;
     huePubSub.publish<ExecutableUpdatedEvent>(
     huePubSub.publish<ExecutableUpdatedEvent>(
       EXECUTABLE_UPDATED_TOPIC,
       EXECUTABLE_UPDATED_TOPIC,
-      mockExecutable as Executable
+      mockExecutable as SqlExecutable
     );
     );
     await nextTick();
     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 { defineComponent, PropType, ref, toRefs, watch } from 'vue';
 
 
   import SubscriptionTracker from 'components/utils/SubscriptionTracker';
   import SubscriptionTracker from 'components/utils/SubscriptionTracker';
-  import Executable, { ExecutionStatus } from '../execution/executable';
+  import SqlExecutable, { ExecutionStatus } from '../execution/sqlExecutable';
 
 
   export default defineComponent({
   export default defineComponent({
     name: 'ExecutableProgressBar',
     name: 'ExecutableProgressBar',
     props: {
     props: {
       executable: {
       executable: {
-        type: Object as PropType<Executable>,
+        type: Object as PropType<SqlExecutable>,
         default: undefined
         default: undefined
       }
       }
     },
     },
@@ -53,7 +53,7 @@
 
 
       let hideTimeout = -1;
       let hideTimeout = -1;
 
 
-      const updateFromExecutable = (updated?: Executable) => {
+      const updateFromExecutable = (updated?: SqlExecutable) => {
         window.clearTimeout(hideTimeout);
         window.clearTimeout(hideTimeout);
         progress.value = (updated && updated.progress) || 0;
         progress.value = (updated && updated.progress) || 0;
         status.value = (updated && updated.status) || ExecutionStatus.ready;
         status.value = (updated && updated.status) || ExecutionStatus.ready;
@@ -69,7 +69,7 @@
       watch(
       watch(
         executable,
         executable,
         newVal => {
         newVal => {
-          updateFromExecutable(newVal as Executable);
+          updateFromExecutable(newVal as SqlExecutable);
         },
         },
         { immediate: true }
         { immediate: true }
       );
       );

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

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

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

@@ -65,9 +65,8 @@
     ExecutableTransitionedEvent,
     ExecutableTransitionedEvent,
     ExecutableUpdatedEvent
     ExecutableUpdatedEvent
   } from 'apps/editor/execution/events';
   } from 'apps/editor/execution/events';
-  import { ExecutionStatus } from 'apps/editor/execution/executable';
   import sessionManager from 'apps/editor/execution/sessionManager';
   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 HueButton from 'components/HueButton.vue';
   import SubscriptionTracker from 'components/utils/SubscriptionTracker';
   import SubscriptionTracker from 'components/utils/SubscriptionTracker';
   import huePubSub from 'utils/huePubSub';
   import huePubSub from 'utils/huePubSub';

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

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

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

@@ -91,7 +91,7 @@
   import ExecutionStatusIcon from './ExecutionStatusIcon.vue';
   import ExecutionStatusIcon from './ExecutionStatusIcon.vue';
   import './QueryHistoryTable.scss';
   import './QueryHistoryTable.scss';
   import { fetchHistory, FetchHistoryResponse } from '../api';
   import { fetchHistory, FetchHistoryResponse } from '../api';
-  import { ExecutionStatus } from '../execution/executable';
+  import { ExecutionStatus } from '../execution/sqlExecutable';
   import { CancellablePromise } from 'api/cancellablePromise';
   import { CancellablePromise } from 'api/cancellablePromise';
   import {
   import {
     EXECUTABLE_TRANSITIONED_TOPIC,
     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
 // See the License for the specific language governing permissions and
 // limitations under the License.
 // limitations under the License.
 
 
-import Executable from '../execution/executable';
+import SqlExecutable from '../execution/sqlExecutable';
 
 
 export const EXECUTE_ACTIVE_EXECUTABLE_TOPIC = 'execute.active.executable';
 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 { defineComponent, computed, onBeforeUnmount, ref, reactive } from 'vue';
 
 
   import { ExecutionJob } from 'apps/editor/execution/api';
   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 { ExecutionError } from 'apps/editor/execution/executionLogs';
   import HueLink from 'components/HueLink.vue';
   import HueLink from 'components/HueLink.vue';
   import LogsPanel from 'components/LogsPanel.vue';
   import LogsPanel from 'components/LogsPanel.vue';
@@ -88,7 +87,7 @@
       const jobsAvailable = computed(() => !!jobs.length);
       const jobsAvailable = computed(() => !!jobs.length);
       const jobsWithUrls = computed(() => jobs.filter(job => job.url));
       const jobsWithUrls = computed(() => jobs.filter(job => job.url));
 
 
-      const debouncedUpdate = debounce((executable: Executable): void => {
+      const debouncedUpdate = debounce((executable: SqlExecutable): void => {
         const { status, logs } = executable;
         const { status, logs } = executable;
         executionLogs.value = logs.fullLog;
         executionLogs.value = logs.fullLog;
         jobs.splice(0, jobs.length, ...logs.jobs);
         jobs.splice(0, jobs.length, ...logs.jobs);
@@ -96,7 +95,7 @@
         analysisAvailable.value = status !== ExecutionStatus.ready || !!errors.length;
         analysisAvailable.value = status !== ExecutionStatus.ready || !!errors.length;
       }, 5);
       }, 5);
 
 
-      const updateFromExecutable = (executable: Executable): void => {
+      const updateFromExecutable = (executable: SqlExecutable): void => {
         if (!props.executable || props.executable.id !== executable.id) {
         if (!props.executable || props.executable.id !== executable.id) {
           return;
           return;
         }
         }

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

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

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

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

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

@@ -28,7 +28,7 @@ import {
 } from './chartTransformers';
 } from './chartTransformers';
 import { attachTracker } from 'apps/editor/components/executableStateHandler';
 import { attachTracker } from 'apps/editor/components/executableStateHandler';
 import { REDRAW_CHART_EVENT } from 'apps/editor/events';
 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 { RESULT_TYPE } from 'apps/editor/execution/executionResult';
 import { CURRENT_QUERY_TAB_SWITCHED_EVENT } from 'apps/editor/snippet';
 import { CURRENT_QUERY_TAB_SWITCHED_EVENT } from 'apps/editor/snippet';
 import componentUtils from 'ko/components/componentUtils';
 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
   SHOW_NORMAL_RESULT_EVENT
 } from 'apps/editor/events';
 } from 'apps/editor/events';
 import { trackResult } from 'apps/editor/components/executableStateHandler';
 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';
 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,
     ExecutableLogsUpdatedEvent,
     ExecutableTransitionedEvent
     ExecutableTransitionedEvent
   } from '../../execution/events';
   } from '../../execution/events';
-  import { ExecutionStatus } from '../../execution/executable';
   import ExecutionLogs from '../../execution/executionLogs';
   import ExecutionLogs from '../../execution/executionLogs';
   import AceEditor from '../aceEditor/AceEditor.vue';
   import AceEditor from '../aceEditor/AceEditor.vue';
   import { ActiveStatementChangedEventDetails } from '../aceEditor/types';
   import { ActiveStatementChangedEventDetails } from '../aceEditor/types';
@@ -88,7 +87,7 @@
   import ExecutionAnalysisPanel from '../executionAnalysis/ExecutionAnalysisPanel.vue';
   import ExecutionAnalysisPanel from '../executionAnalysis/ExecutionAnalysisPanel.vue';
   import ResultTable from '../result/ResultTable.vue';
   import ResultTable from '../result/ResultTable.vue';
   import Executor from '../../execution/executor';
   import Executor from '../../execution/executor';
-  import SqlExecutable from '../../execution/sqlExecutable';
+  import SqlExecutable, { ExecutionStatus } from '../../execution/sqlExecutable';
   import { login } from 'api/auth';
   import { login } from 'api/auth';
   import { setBaseUrl } from 'api/utils';
   import { setBaseUrl } from 'api/utils';
   import contextCatalog from 'catalog/contextCatalog';
   import contextCatalog from 'catalog/contextCatalog';

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

@@ -23,9 +23,11 @@ import {
   ExecutableTransitionedEvent
   ExecutableTransitionedEvent
 } from './events';
 } from './events';
 import Executor from './executor';
 import Executor from './executor';
-import SqlExecutable from './sqlExecutable';
 import { DefaultApiResponse, extractErrorMessage, post, successResponseIsError } from 'api/utils';
 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 { ResultRow, ResultType } from 'apps/editor/execution/executionResult';
 import { CancellablePromise } from 'api/cancellablePromise';
 import { CancellablePromise } from 'api/cancellablePromise';
 import SubscriptionTracker from 'components/utils/SubscriptionTracker';
 import SubscriptionTracker from 'components/utils/SubscriptionTracker';
@@ -143,7 +145,7 @@ export interface ExecuteStatusApiResponse {
 }
 }
 
 
 export interface ExecuteApiOptions {
 export interface ExecuteApiOptions {
-  executable: Executable;
+  executable: SqlExecutable;
   silenceErrors?: boolean;
   silenceErrors?: boolean;
 }
 }
 
 
@@ -410,7 +412,7 @@ export const checkExecutionStatus = async (
 };
 };
 
 
 export const fetchResults = async (options: {
 export const fetchResults = async (options: {
-  executable: Executable;
+  executable: SqlExecutable;
   rows: number;
   rows: number;
   startOver?: boolean;
   startOver?: boolean;
   silenceErrors?: boolean;
   silenceErrors?: boolean;
@@ -455,7 +457,7 @@ export const fetchResultSize = async (
 };
 };
 
 
 export const fetchLogs = async (options: {
 export const fetchLogs = async (options: {
-  executable: Executable;
+  executable: SqlExecutable;
   silenceErrors?: boolean;
   silenceErrors?: boolean;
   fullLog: string;
   fullLog: string;
   jobs?: ExecutionJob[];
   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 { Session } from 'apps/editor/execution/api';
 import ExecutionResult from './executionResult';
 import ExecutionResult from './executionResult';
-import Executable, { ExecutionStatus } from './executable';
+import SqlExecutable, { ExecutionStatus } from './sqlExecutable';
 import ExecutionLogs from './executionLogs';
 import ExecutionLogs from './executionLogs';
 
 
 export const EXECUTABLE_TRANSITIONED_TOPIC = 'hue.executable.status.transitioned';
 export const EXECUTABLE_TRANSITIONED_TOPIC = 'hue.executable.status.transitioned';
 export interface ExecutableTransitionedEvent {
 export interface ExecutableTransitionedEvent {
   newStatus: ExecutionStatus;
   newStatus: ExecutionStatus;
   oldStatus: ExecutionStatus;
   oldStatus: ExecutionStatus;
-  executable: Executable;
+  executable: SqlExecutable;
 }
 }
 
 
 export const EXECUTABLE_UPDATED_TOPIC = 'hue.executable.updated';
 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 const EXECUTABLE_LOGS_UPDATED_TOPIC = 'hue.executable.logs.updated';
 export type ExecutableLogsUpdatedEvent = ExecutionLogs;
 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
   ExecutableLogsUpdatedEvent
 } from 'apps/editor/execution/events';
 } from 'apps/editor/execution/events';
 import huePubSub from 'utils/huePubSub';
 import huePubSub from 'utils/huePubSub';
-import Executable, { ExecutionStatus } from './executable';
+import SqlExecutable, { ExecutionStatus } from './sqlExecutable';
 
 
 export interface ExecutionError {
 export interface ExecutionError {
   row: number;
   row: number;
@@ -34,13 +34,13 @@ export interface ExecutionLogsRaw {
 }
 }
 
 
 export default class ExecutionLogs {
 export default class ExecutionLogs {
-  executable: Executable;
+  executable: SqlExecutable;
   fullLog = '';
   fullLog = '';
   logLines = 0;
   logLines = 0;
   jobs: ExecutionJob[] = [];
   jobs: ExecutionJob[] = [];
   errors: ExecutionError[] = [];
   errors: ExecutionError[] = [];
 
 
-  constructor(executable: Executable) {
+  constructor(executable: SqlExecutable) {
     this.executable = executable;
     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 huePubSub from 'utils/huePubSub';
 import sleep from 'utils/timing/sleep';
 import sleep from 'utils/timing/sleep';
-import Executable, { ExecutionStatus } from './executable';
+import SqlExecutable, { ExecutionStatus } from './sqlExecutable';
 
 
 export const RESULT_TYPE = {
 export const RESULT_TYPE = {
   TABLE: 'table'
   TABLE: 'table'
@@ -96,7 +96,7 @@ export enum ResultType {
 }
 }
 
 
 export default class ExecutionResult {
 export default class ExecutionResult {
-  executable: Executable;
+  executable: SqlExecutable;
   streaming: boolean;
   streaming: boolean;
 
 
   type?: ResultType;
   type?: ResultType;
@@ -115,7 +115,7 @@ export default class ExecutionResult {
   isEscaped = false;
   isEscaped = false;
   fetchedOnce = false;
   fetchedOnce = false;
 
 
-  constructor(executable: Executable, streaming?: boolean) {
+  constructor(executable: SqlExecutable, streaming?: boolean) {
     this.executable = executable;
     this.executable = executable;
     this.streaming = !!streaming;
     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 KnockoutObservable from '@types/knockout';
 
 
 import Snippet from 'apps/editor/snippet';
 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 { syncSqlExecutables } from 'apps/editor/execution/utils';
 import { StatementDetails } from 'parse/types';
 import { StatementDetails } from 'parse/types';
 import { Compute, Connector, Namespace } from 'config/types';
 import { Compute, Connector, Namespace } from 'config/types';
@@ -46,9 +46,9 @@ export default class Executor {
   defaultLimit?: KnockoutObservable<number>;
   defaultLimit?: KnockoutObservable<number>;
   isSqlEngine?: boolean;
   isSqlEngine?: boolean;
   isSqlAnalyzerEnabled?: boolean;
   isSqlAnalyzerEnabled?: boolean;
-  executables: Executable[] = [];
+  executables: SqlExecutable[] = [];
   snippet?: Snippet;
   snippet?: Snippet;
-  activeExecutable?: Executable;
+  activeExecutable?: SqlExecutable;
   variables: VariableIndex = {};
   variables: VariableIndex = {};
 
 
   constructor(options: ExecutorOptions) {
   constructor(options: ExecutorOptions) {
@@ -73,13 +73,13 @@ export default class Executor {
     this.executables.forEach(existingExecutable => existingExecutable.cancelBatchChain());
     this.executables.forEach(existingExecutable => existingExecutable.cancelBatchChain());
   }
   }
 
 
-  setExecutables(executables: Executable[]): void {
+  setExecutables(executables: SqlExecutable[]): void {
     this.cancelAll();
     this.cancelAll();
     this.executables = executables;
     this.executables = executables;
     this.executables.forEach(executable => executable.notify());
     this.executables.forEach(executable => executable.notify());
   }
   }
 
 
-  update(statementDetails: StatementDetails, beforeExecute: boolean): Executable {
+  update(statementDetails: StatementDetails, beforeExecute: boolean): SqlExecutable {
     const executables = syncSqlExecutables(this, statementDetails);
     const executables = syncSqlExecutables(this, statementDetails);
 
 
     // Cancel any "lost" executables and any batch chain it's part of
     // Cancel any "lost" executables and any batch chain it's part of
@@ -92,7 +92,7 @@ export default class Executor {
     if (beforeExecute) {
     if (beforeExecute) {
       executables.selected.forEach(executable => executable.cancelBatchChain());
       executables.selected.forEach(executable => executable.cancelBatchChain());
 
 
-      let previous: Executable | undefined;
+      let previous: SqlExecutable | undefined;
       executables.selected.forEach(executable => {
       executables.selected.forEach(executable => {
         if (previous) {
         if (previous) {
           executable.previousExecutable = 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 Executor from 'apps/editor/execution/executor';
 import SqlExecutable from './sqlExecutable';
 import SqlExecutable from './sqlExecutable';
-import { ExecutionStatus } from './executable';
+import { ExecutionStatus } from './sqlExecutable';
 import sessionManager from './sessionManager';
 import sessionManager from './sessionManager';
 import * as ApiUtils from 'api/utils';
 import * as ApiUtils from 'api/utils';
 import { ParsedSqlStatement } from 'parse/sqlStatementsParser';
 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
 // See the License for the specific language governing permissions and
 // limitations under the License.
 // 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 Executor from 'apps/editor/execution/executor';
+import sessionManager from 'apps/editor/execution/sessionManager';
+import dataCatalog from 'catalog/dataCatalog';
 import { ParsedSqlStatement } from 'parse/sqlStatementsParser';
 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 =
 const BATCHABLE_STATEMENT_TYPES =
   /ALTER|ANALYZE|WITH|REFRESH|CREATE|DELETE|DROP|GRANT|INSERT|INVALIDATE|LOAD|SET|TRUNCATE|UPDATE|UPSERT|USE/i;
   /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 =
 const DB_DDL_REGEX =
   /(?:CREATE|DROP)\s+(?:DATABASE|SCHEMA)\s+(?:IF\s+(?:NOT\s+)?EXISTS\s+)?(?:`([^`]+)`|([^;\s]+))/i;
   /(?: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 => {
 const substituteVariables = (statement: string, variables: VariableIndex): string => {
   if (!Object.keys(variables).length) {
   if (!Object.keys(variables).length) {
     return statement;
     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;
   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;
   parsedStatement: ParsedSqlStatement;
 
 
   constructor(options: {
   constructor(options: {
@@ -66,41 +135,263 @@ export default class SqlExecutable extends Executable {
     database: string;
     database: string;
     parsedStatement: ParsedSqlStatement;
     parsedStatement: ParsedSqlStatement;
   }) {
   }) {
-    super(options);
+    this.logs = new ExecutionLogs(this);
+    this.executor = options.executor;
     this.database = options.database;
     this.database = options.database;
     this.parsedStatement = options.parsedStatement;
     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> {
   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 {
   canExecuteInBatch(): boolean {
     return this.parsedStatement && BATCHABLE_STATEMENT_TYPES.test(this.parsedStatement.firstToken);
     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 {
   static fromJs(executor: Executor, executableRaw: SqlExecutableRaw): SqlExecutable {
     const executable = new SqlExecutable({
     const executable = new SqlExecutable({
       database: executableRaw.database,
       database: executableRaw.database,
@@ -171,30 +547,95 @@ export default class SqlExecutable extends Executable {
   }
   }
 
 
   toJs(): SqlExecutableRaw {
   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,
       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
 // See the License for the specific language governing permissions and
 // limitations under the License.
 // limitations under the License.
 
 
-import Executable from 'apps/editor/execution/executable';
 import Executor from 'apps/editor/execution/executor';
 import Executor from 'apps/editor/execution/executor';
 import SqlExecutable from 'apps/editor/execution/sqlExecutable';
 import SqlExecutable from 'apps/editor/execution/sqlExecutable';
 import { StatementDetails } from 'parse/types';
 import { StatementDetails } from 'parse/types';
@@ -37,7 +36,7 @@ export const syncSqlExecutables = (
     ...statementDetails.followingStatements
     ...statementDetails.followingStatements
   ];
   ];
 
 
-  const existingExecutables: (Executable | undefined)[] = [...executor.executables];
+  const existingExecutables: (SqlExecutable | undefined)[] = [...executor.executables];
 
 
   const result = {
   const result = {
     all: <SqlExecutable[]>[],
     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 UUID from 'utils/string/UUID';
 import { getFromLocalStorage, setInLocalStorage } from 'utils/storageUtils';
 import { getFromLocalStorage, setInLocalStorage } from 'utils/storageUtils';
 import sessionManager from 'apps/editor/execution/sessionManager';
 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 { HIDE_FIXED_HEADERS_EVENT, REDRAW_FIXED_HEADERS_EVENT } from 'apps/editor/events';
-import { ExecutionStatus } from 'apps/editor/execution/executable';
 import {
 import {
   ACTIVE_STATEMENT_CHANGED_EVENT,
   ACTIVE_STATEMENT_CHANGED_EVENT,
   REFRESH_STATEMENT_LOCATIONS_EVENT
   REFRESH_STATEMENT_LOCATIONS_EVENT