Initial Code Layering (#3788)

* working on formatting

* fixed basic lint errors; starting moving things to their appropriate location

* formatting

* update tslint to match the version of vscode we have

* remove unused code

* work in progress fixing layering

* formatting

* moved connection management service to platform

* formatting

* add missing file

* moving more servies

* formatting

* moving more services

* formatting

* wip

* moving more services

* formatting

* revert back tslint rules

* move css file

* add missing svgs
This commit is contained in:
Anthony Dresser
2019-01-25 14:52:35 -08:00
committed by GitHub
parent c8986464ec
commit ea67859de7
338 changed files with 2036 additions and 7386 deletions

View File

@@ -0,0 +1,326 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import QueryRunner from 'sql/platform/query/common/queryRunner';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IDisposable } from 'vs/base/common/lifecycle';
import * as sqlops from 'sqlops';
import { TPromise } from 'vs/base/common/winjs.base';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
import * as TelemetryUtils from 'sql/common/telemetryUtilities';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
export const SERVICE_ID = 'queryManagementService';
export const IQueryManagementService = createDecorator<IQueryManagementService>(SERVICE_ID);
export interface IQueryManagementService {
_serviceBrand: any;
addQueryRequestHandler(queryType: string, runner: IQueryRequestHandler): IDisposable;
isProviderRegistered(providerId: string): boolean;
registerRunner(runner: QueryRunner, uri: string): void;
cancelQuery(ownerUri: string): Thenable<sqlops.QueryCancelResult>;
runQuery(ownerUri: string, selection: sqlops.ISelectionData, runOptions?: sqlops.ExecutionPlanOptions): Thenable<void>;
runQueryStatement(ownerUri: string, line: number, column: number): Thenable<void>;
runQueryString(ownerUri: string, queryString: string): Thenable<void>;
runQueryAndReturn(ownerUri: string, queryString: string): Thenable<sqlops.SimpleExecuteResult>;
parseSyntax(ownerUri: string, query: string): Thenable<sqlops.SyntaxParseResult>;
getQueryRows(rowData: sqlops.QueryExecuteSubsetParams): Thenable<sqlops.QueryExecuteSubsetResult>;
disposeQuery(ownerUri: string): Thenable<void>;
saveResults(requestParams: sqlops.SaveResultsRequestParams): Thenable<sqlops.SaveResultRequestResult>;
// Callbacks
onQueryComplete(result: sqlops.QueryExecuteCompleteNotificationResult): void;
onBatchStart(batchInfo: sqlops.QueryExecuteBatchNotificationParams): void;
onBatchComplete(batchInfo: sqlops.QueryExecuteBatchNotificationParams): void;
onResultSetAvailable(resultSetInfo: sqlops.QueryExecuteResultSetNotificationParams): void;
onResultSetUpdated(resultSetInfo: sqlops.QueryExecuteResultSetNotificationParams): void;
onMessage(message: sqlops.QueryExecuteMessageParams): void;
// Edit Data Callbacks
onEditSessionReady(ownerUri: string, success: boolean, message: string): void;
// Edit Data Functions
initializeEdit(ownerUri: string, schemaName: string, objectName: string, objectType: string, rowLimit: number, queryString: string): Thenable<void>;
disposeEdit(ownerUri: string): Thenable<void>;
updateCell(ownerUri: string, rowId: number, columnId: number, newValue: string): Thenable<sqlops.EditUpdateCellResult>;
commitEdit(ownerUri): Thenable<void>;
createRow(ownerUri: string): Thenable<sqlops.EditCreateRowResult>;
deleteRow(ownerUri: string, rowId: number): Thenable<void>;
revertCell(ownerUri: string, rowId: number, columnId: number): Thenable<sqlops.EditRevertCellResult>;
revertRow(ownerUri: string, rowId: number): Thenable<void>;
getEditRows(rowData: sqlops.EditSubsetParams): Thenable<sqlops.EditSubsetResult>;
}
/*
* An object that can handle basic request-response actions related to queries
*/
export interface IQueryRequestHandler {
cancelQuery(ownerUri: string): Thenable<sqlops.QueryCancelResult>;
runQuery(ownerUri: string, selection: sqlops.ISelectionData, runOptions?: sqlops.ExecutionPlanOptions): Thenable<void>;
runQueryStatement(ownerUri: string, line: number, column: number): Thenable<void>;
runQueryString(ownerUri: string, queryString: string): Thenable<void>;
runQueryAndReturn(ownerUri: string, queryString: string): Thenable<sqlops.SimpleExecuteResult>;
parseSyntax(ownerUri: string, query: string): Thenable<sqlops.SyntaxParseResult>;
getQueryRows(rowData: sqlops.QueryExecuteSubsetParams): Thenable<sqlops.QueryExecuteSubsetResult>;
disposeQuery(ownerUri: string): Thenable<void>;
saveResults(requestParams: sqlops.SaveResultsRequestParams): Thenable<sqlops.SaveResultRequestResult>;
// Edit Data actions
initializeEdit(ownerUri: string, schemaName: string, objectName: string, objectType: string, rowLimit: number, queryString: string): Thenable<void>;
disposeEdit(ownerUri: string): Thenable<void>;
updateCell(ownerUri: string, rowId: number, columnId: number, newValue: string): Thenable<sqlops.EditUpdateCellResult>;
commitEdit(ownerUri): Thenable<void>;
createRow(ownerUri: string): Thenable<sqlops.EditCreateRowResult>;
deleteRow(ownerUri: string, rowId: number): Thenable<void>;
revertCell(ownerUri: string, rowId: number, columnId: number): Thenable<sqlops.EditRevertCellResult>;
revertRow(ownerUri: string, rowId: number): Thenable<void>;
getEditRows(rowData: sqlops.EditSubsetParams): Thenable<sqlops.EditSubsetResult>;
}
export class QueryManagementService implements IQueryManagementService {
public _serviceBrand: any;
private _requestHandlers = new Map<string, IQueryRequestHandler>();
// public for testing only
public _queryRunners = new Map<string, QueryRunner>();
// public for testing only
public _handlerCallbackQueue: ((run: QueryRunner) => void)[] = [];
constructor(
@IConnectionManagementService private _connectionService: IConnectionManagementService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
}
// Registers queryRunners with their uris to distribute notifications.
// Ensures that notifications are handled in the correct order by handling
// enqueued handlers first.
// public for testing only
public registerRunner(runner: QueryRunner, uri: string): void {
// If enqueueOrRun was called before registerRunner for the current query,
// _handlerCallbackQueue will be non-empty. Run all handlers in the queue first
// so that notifications are handled in order they arrived
while (this._handlerCallbackQueue.length > 0) {
let handler = this._handlerCallbackQueue.shift();
handler(runner);
}
// Set the runner for any other handlers if the runner is in use by the
// current query or a subsequent query
if (!runner.hasCompleted) {
this._queryRunners.set(uri, runner);
}
}
// Handles logic to run the given handlerCallback at the appropriate time. If the given runner is
// undefined, the handlerCallback is put on the _handlerCallbackQueue to be run once the runner is set
// public for testing only
private enqueueOrRun(handlerCallback: (runnerParam: QueryRunner) => void, runner: QueryRunner): void {
if (runner === undefined) {
this._handlerCallbackQueue.push(handlerCallback);
} else {
handlerCallback(runner);
}
}
private _notify(ownerUri: string, sendNotification: (runner: QueryRunner) => void): void {
let runner = this._queryRunners.get(ownerUri);
this.enqueueOrRun(sendNotification, runner);
}
public addQueryRequestHandler(queryType: string, handler: IQueryRequestHandler): IDisposable {
this._requestHandlers.set(queryType, handler);
return {
dispose: () => {
}
};
}
public isProviderRegistered(providerId: string): boolean {
let handler = this._requestHandlers.get(providerId);
return !!handler;
}
private addTelemetry(eventName: string, ownerUri: string, runOptions?: sqlops.ExecutionPlanOptions): void {
let providerId: string = this._connectionService.getProviderIdFromUri(ownerUri);
let data: TelemetryUtils.IConnectionTelemetryData = {
provider: providerId,
};
if (runOptions) {
data = Object.assign({}, data, {
displayEstimatedQueryPlan: runOptions.displayEstimatedQueryPlan,
displayActualQueryPlan: runOptions.displayActualQueryPlan
});
}
TelemetryUtils.addTelemetry(this._telemetryService, eventName, data);
}
private _runAction<T>(uri: string, action: (handler: IQueryRequestHandler) => Thenable<T>): Thenable<T> {
let providerId: string = this._connectionService.getProviderIdFromUri(uri);
if (!providerId) {
return TPromise.wrapError(new Error('Connection is required in order to interact with queries'));
}
let handler = this._requestHandlers.get(providerId);
if (handler) {
return action(handler);
} else {
return TPromise.wrapError(new Error('No Handler Registered'));
}
}
public cancelQuery(ownerUri: string): Thenable<sqlops.QueryCancelResult> {
this.addTelemetry(TelemetryKeys.CancelQuery, ownerUri);
return this._runAction(ownerUri, (runner) => {
return runner.cancelQuery(ownerUri);
});
}
public runQuery(ownerUri: string, selection: sqlops.ISelectionData, runOptions?: sqlops.ExecutionPlanOptions): Thenable<void> {
this.addTelemetry(TelemetryKeys.RunQuery, ownerUri, runOptions);
return this._runAction(ownerUri, (runner) => {
return runner.runQuery(ownerUri, selection, runOptions);
});
}
public runQueryStatement(ownerUri: string, line: number, column: number): Thenable<void> {
this.addTelemetry(TelemetryKeys.RunQueryStatement, ownerUri);
return this._runAction(ownerUri, (runner) => {
return runner.runQueryStatement(ownerUri, line, column);
});
}
public runQueryString(ownerUri: string, queryString: string): Thenable<void> {
return this._runAction(ownerUri, (runner) => {
return runner.runQueryString(ownerUri, queryString);
});
}
public runQueryAndReturn(ownerUri: string, queryString: string): Thenable<sqlops.SimpleExecuteResult> {
return this._runAction(ownerUri, (runner) => {
return runner.runQueryAndReturn(ownerUri, queryString);
});
}
public parseSyntax(ownerUri: string, query: string): Thenable<sqlops.SyntaxParseResult> {
return this._runAction(ownerUri, (runner) => {
return runner.parseSyntax(ownerUri, query);
});
}
public getQueryRows(rowData: sqlops.QueryExecuteSubsetParams): Thenable<sqlops.QueryExecuteSubsetResult> {
return this._runAction(rowData.ownerUri, (runner) => {
return runner.getQueryRows(rowData);
});
}
public disposeQuery(ownerUri: string): Thenable<void> {
this._queryRunners.delete(ownerUri);
return this._runAction(ownerUri, (runner) => {
return runner.disposeQuery(ownerUri);
});
}
public saveResults(requestParams: sqlops.SaveResultsRequestParams): Thenable<sqlops.SaveResultRequestResult> {
return this._runAction(requestParams.ownerUri, (runner) => {
return runner.saveResults(requestParams);
});
}
public onQueryComplete(result: sqlops.QueryExecuteCompleteNotificationResult): void {
this._notify(result.ownerUri, (runner: QueryRunner) => {
runner.handleQueryComplete(result);
});
}
public onBatchStart(batchInfo: sqlops.QueryExecuteBatchNotificationParams): void {
this._notify(batchInfo.ownerUri, (runner: QueryRunner) => {
runner.handleBatchStart(batchInfo);
});
}
public onBatchComplete(batchInfo: sqlops.QueryExecuteBatchNotificationParams): void {
this._notify(batchInfo.ownerUri, (runner: QueryRunner) => {
runner.handleBatchComplete(batchInfo);
});
}
public onResultSetAvailable(resultSetInfo: sqlops.QueryExecuteResultSetNotificationParams): void {
this._notify(resultSetInfo.ownerUri, (runner: QueryRunner) => {
runner.handleResultSetAvailable(resultSetInfo);
});
}
public onResultSetUpdated(resultSetInfo: sqlops.QueryExecuteResultSetNotificationParams): void {
this._notify(resultSetInfo.ownerUri, (runner: QueryRunner) => {
runner.handleResultSetUpdated(resultSetInfo);
});
}
public onMessage(message: sqlops.QueryExecuteMessageParams): void {
this._notify(message.ownerUri, (runner: QueryRunner) => {
runner.handleMessage(message);
});
}
// Edit Data Functions
public initializeEdit(ownerUri: string, schemaName: string, objectName: string, objectType: string, rowLimit: number, queryString: string): Thenable<void> {
return this._runAction(ownerUri, (runner) => {
return runner.initializeEdit(ownerUri, schemaName, objectName, objectType, rowLimit, queryString);
});
}
public onEditSessionReady(ownerUri: string, success: boolean, message: string): void {
this._notify(ownerUri, (runner: QueryRunner) => {
runner.handleEditSessionReady(ownerUri, success, message);
});
}
public updateCell(ownerUri: string, rowId: number, columnId: number, newValue: string): Thenable<sqlops.EditUpdateCellResult> {
return this._runAction(ownerUri, (runner) => {
return runner.updateCell(ownerUri, rowId, columnId, newValue);
});
}
public commitEdit(ownerUri: string): Thenable<void> {
return this._runAction(ownerUri, (runner) => {
return runner.commitEdit(ownerUri);
});
}
public createRow(ownerUri: string): Thenable<sqlops.EditCreateRowResult> {
return this._runAction(ownerUri, (runner) => {
return runner.createRow(ownerUri);
});
}
public deleteRow(ownerUri: string, rowId: number): Thenable<void> {
return this._runAction(ownerUri, (runner) => {
return runner.deleteRow(ownerUri, rowId);
});
}
public disposeEdit(ownerUri: string): Thenable<void> {
return this._runAction(ownerUri, (runner) => {
return runner.disposeEdit(ownerUri);
});
}
public revertCell(ownerUri: string, rowId: number, columnId: number): Thenable<sqlops.EditRevertCellResult> {
return this._runAction(ownerUri, (runner) => {
return runner.revertCell(ownerUri, rowId, columnId);
});
}
public revertRow(ownerUri: string, rowId: number): Thenable<void> {
return this._runAction(ownerUri, (runner) => {
return runner.revertRow(ownerUri, rowId);
});
}
public getEditRows(rowData: sqlops.EditSubsetParams): Thenable<sqlops.EditSubsetResult> {
return this._runAction(rowData.ownerUri, (runner) => {
return runner.getEditRows(rowData);
});
}
}

View File

@@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import QueryRunner from 'sql/platform/query/common/queryRunner';
import { DataService } from 'sql/parts/grid/services/dataService';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import { QueryInput } from 'sql/parts/query/common/queryInput';
import {
ISelectionData,
ResultSetSubset,
EditUpdateCellResult,
EditSessionReadyParams,
EditSubsetResult,
EditCreateRowResult,
EditRevertCellResult,
ExecutionPlanOptions
} from 'sqlops';
import { QueryInfo } from 'sql/platform/query/common/queryModelService';
export const SERVICE_ID = 'queryModelService';
export const IQueryModelService = createDecorator<IQueryModelService>(SERVICE_ID);
/**
* Interface for the logic of handling running queries and grid interactions for all URIs.
*/
export interface IQueryModelService {
_serviceBrand: any;
getQueryRunner(uri: string): QueryRunner;
getConfig(): Promise<{ [key: string]: any }>;
getShortcuts(): Promise<any>;
getQueryRows(uri: string, rowStart: number, numberOfRows: number, batchId: number, resultId: number): Thenable<ResultSetSubset>;
runQuery(uri: string, selection: ISelectionData, queryInput: QueryInput, runOptions?: ExecutionPlanOptions): void;
runQueryStatement(uri: string, selection: ISelectionData, queryInput: QueryInput): void;
runQueryString(uri: string, selection: string, queryInput: QueryInput);
cancelQuery(input: QueryRunner | string): void;
disposeQuery(uri: string): void;
isRunningQuery(uri: string): boolean;
getDataService(uri: string): DataService;
refreshResultsets(uri: string): void;
sendGridContentEvent(uri: string, eventName: string): void;
resizeResultsets(uri: string): void;
onAngularLoaded(uri: string): void;
copyResults(uri: string, selection: Slick.Range[], batchId: number, resultId: number, includeHeaders?: boolean): void;
setEditorSelection(uri: string, index: number): void;
showWarning(uri: string, message: string): void;
showError(uri: string, message: string): void;
showCommitError(error: string): void;
onRunQueryStart: Event<string>;
onRunQueryComplete: Event<string>;
// Edit Data Functions
initializeEdit(ownerUri: string, schemaName: string, objectName: string, objectType: string, rowLimit: number, queryString: string): void;
disposeEdit(ownerUri: string): Thenable<void>;
updateCell(ownerUri: string, rowId: number, columnId: number, newValue: string): Thenable<EditUpdateCellResult>;
commitEdit(ownerUri): Thenable<void>;
createRow(ownerUri: string): Thenable<EditCreateRowResult>;
deleteRow(ownerUri: string, rowId: number): Thenable<void>;
revertCell(ownerUri: string, rowId: number, columnId: number): Thenable<EditRevertCellResult>;
revertRow(ownerUri: string, rowId: number): Thenable<void>;
getEditRows(ownerUri: string, rowStart: number, numberOfRows: number): Thenable<EditSubsetResult>;
_getQueryInfo(uri: string): QueryInfo;
// Edit Data Callbacks
onEditSessionReady: Event<EditSessionReadyParams>;
}

View File

@@ -0,0 +1,607 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as GridContentEvents from 'sql/parts/grid/common/gridContentEvents';
import * as LocalizedConstants from 'sql/parts/query/common/localizedConstants';
import QueryRunner, { EventType as QREvents } from 'sql/platform/query/common/queryRunner';
import { DataService } from 'sql/parts/grid/services/dataService';
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
import { QueryInput } from 'sql/parts/query/common/queryInput';
import { QueryStatusbarItem } from 'sql/parts/query/execution/queryStatus';
import { SqlFlavorStatusbarItem } from 'sql/parts/query/common/flavorStatus';
import { RowCountStatusBarItem } from 'sql/parts/query/common/rowCountStatus';
import { TimeElapsedStatusBarItem } from 'sql/parts/query/common/timeElapsedStatus';
import * as sqlops from 'sqlops';
import * as nls from 'vs/nls';
import * as statusbar from 'vs/workbench/browser/parts/statusbar/statusbar';
import * as platform from 'vs/platform/registry/common/platform';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Event, Emitter } from 'vs/base/common/event';
import { TPromise } from 'vs/base/common/winjs.base';
import * as strings from 'vs/base/common/strings';
import * as types from 'vs/base/common/types';
import { INotificationService } from 'vs/platform/notification/common/notification';
import Severity from 'vs/base/common/severity';
const selectionSnippetMaxLen = 100;
export interface QueryEvent {
type: string;
data: any;
}
/**
* Holds information about the state of a query runner
*/
export class QueryInfo {
public queryRunner: QueryRunner;
public dataService: DataService;
public queryEventQueue: QueryEvent[];
public selection: Array<sqlops.ISelectionData>;
public queryInput: QueryInput;
public selectionSnippet: string;
// Notes if the angular components have obtained the DataService. If not, all messages sent
// via the data service will be lost.
public dataServiceReady: boolean;
constructor() {
this.dataServiceReady = false;
this.queryEventQueue = [];
this.selection = [];
}
}
/**
* Handles running queries and grid interactions for all URIs. Interacts with each URI's results grid via a DataService instance
*/
export class QueryModelService implements IQueryModelService {
_serviceBrand: any;
// MEMBER VARIABLES ////////////////////////////////////////////////////
private _queryInfoMap: Map<string, QueryInfo>;
private _onRunQueryStart: Emitter<string>;
private _onRunQueryComplete: Emitter<string>;
private _onEditSessionReady: Emitter<sqlops.EditSessionReadyParams>;
// EVENTS /////////////////////////////////////////////////////////////
public get onRunQueryStart(): Event<string> { return this._onRunQueryStart.event; }
public get onRunQueryComplete(): Event<string> { return this._onRunQueryComplete.event; }
public get onEditSessionReady(): Event<sqlops.EditSessionReadyParams> { return this._onEditSessionReady.event; }
// CONSTRUCTOR /////////////////////////////////////////////////////////
constructor(
@IInstantiationService private _instantiationService: IInstantiationService,
@INotificationService private _notificationService: INotificationService
) {
this._queryInfoMap = new Map<string, QueryInfo>();
this._onRunQueryStart = new Emitter<string>();
this._onRunQueryComplete = new Emitter<string>();
this._onEditSessionReady = new Emitter<sqlops.EditSessionReadyParams>();
// Register Statusbar items
(<statusbar.IStatusbarRegistry>platform.Registry.as(statusbar.Extensions.Statusbar)).registerStatusbarItem(new statusbar.StatusbarItemDescriptor(
TimeElapsedStatusBarItem,
statusbar.StatusbarAlignment.RIGHT,
100 /* Should appear to the right of the SQL editor status */
));
(<statusbar.IStatusbarRegistry>platform.Registry.as(statusbar.Extensions.Statusbar)).registerStatusbarItem(new statusbar.StatusbarItemDescriptor(
RowCountStatusBarItem,
statusbar.StatusbarAlignment.RIGHT,
100 /* Should appear to the right of the SQL editor status */
));
(<statusbar.IStatusbarRegistry>platform.Registry.as(statusbar.Extensions.Statusbar)).registerStatusbarItem(new statusbar.StatusbarItemDescriptor(
QueryStatusbarItem,
statusbar.StatusbarAlignment.RIGHT,
100 /* High Priority */
));
(<statusbar.IStatusbarRegistry>platform.Registry.as(statusbar.Extensions.Statusbar)).registerStatusbarItem(new statusbar.StatusbarItemDescriptor(
SqlFlavorStatusbarItem,
statusbar.StatusbarAlignment.RIGHT,
90 /* Should appear to the right of the SQL editor status */
));
}
// IQUERYMODEL /////////////////////////////////////////////////////////
public getDataService(uri: string): DataService {
let dataService = this._getQueryInfo(uri).dataService;
if (!dataService) {
throw new Error('Could not find data service for uri: ' + uri);
}
return dataService;
}
/**
* Force all grids to re-render. This is needed to re-render the grids when switching
* between different URIs.
*/
public refreshResultsets(uri: string): void {
this._fireGridContentEvent(uri, GridContentEvents.RefreshContents);
}
/**
* Resize the grid UI to fit the current screen size.
*/
public resizeResultsets(uri: string): void {
this._fireGridContentEvent(uri, GridContentEvents.ResizeContents);
}
public sendGridContentEvent(uri: string, eventName: string): void {
this._fireGridContentEvent(uri, eventName);
}
/**
* To be called by an angular component's DataService when the component has finished loading.
* Sends all previously enqueued query events to the DataService and signals to stop enqueuing
* any further events. This prevents QueryEvents from getting lost if they are sent before
* angular is listening for them.
*/
public onAngularLoaded(uri: string) {
let info = this._getQueryInfo(uri);
info.dataServiceReady = true;
this._sendQueuedEvents(uri);
}
/**
* Get more data rows from the current resultSets from the service layer
*/
public getQueryRows(uri: string, rowStart: number, numberOfRows: number, batchId: number, resultId: number): Thenable<sqlops.ResultSetSubset> {
return this._getQueryInfo(uri).queryRunner.getQueryRows(rowStart, numberOfRows, batchId, resultId).then(results => {
return results.resultSubset;
});
}
public getEditRows(uri: string, rowStart: number, numberOfRows: number): Thenable<sqlops.EditSubsetResult> {
return this._queryInfoMap.get(uri).queryRunner.getEditRows(rowStart, numberOfRows).then(results => {
return results;
});
}
public getConfig(): Promise<{ [key: string]: any }> {
return undefined;
}
public getShortcuts(): Promise<any> {
return undefined;
}
public copyResults(uri: string, selection: Slick.Range[], batchId: number, resultId: number, includeHeaders?: boolean): void {
this._queryInfoMap.get(uri).queryRunner.copyResults(selection, batchId, resultId, includeHeaders);
}
public setEditorSelection(uri: string, index: number): void {
let info: QueryInfo = this._queryInfoMap.get(uri);
if (info && info.queryInput) {
info.queryInput.updateSelection(info.selection[index]);
}
}
public showWarning(uri: string, message: string): void {
}
public showError(uri: string, message: string): void {
}
public showCommitError(error: string): void {
this._notificationService.notify({
severity: Severity.Error,
message: nls.localize('commitEditFailed', 'Commit row failed: ') + error
});
}
public isRunningQuery(uri: string): boolean {
return !this._queryInfoMap.has(uri)
? false
: this._getQueryInfo(uri).queryRunner.isExecuting;
}
/**
* Run a query for the given URI with the given text selection
*/
public runQuery(uri: string, selection: sqlops.ISelectionData, queryInput: QueryInput, runOptions?: sqlops.ExecutionPlanOptions): void {
this.doRunQuery(uri, selection, queryInput, false, runOptions);
}
/**
* Run the current SQL statement for the given URI
*/
public runQueryStatement(uri: string, selection: sqlops.ISelectionData, queryInput: QueryInput): void {
this.doRunQuery(uri, selection, queryInput, true);
}
/**
* Run the current SQL statement for the given URI
*/
public runQueryString(uri: string, selection: string, queryInput: QueryInput): void {
this.doRunQuery(uri, selection, queryInput, true);
}
/**
* Run Query implementation
*/
private doRunQuery(uri: string, selection: sqlops.ISelectionData | string, queryInput: QueryInput,
runCurrentStatement: boolean, runOptions?: sqlops.ExecutionPlanOptions): void {
// Reuse existing query runner if it exists
let queryRunner: QueryRunner;
let info: QueryInfo;
if (this._queryInfoMap.has(uri)) {
info = this._getQueryInfo(uri);
let existingRunner: QueryRunner = info.queryRunner;
// If the query is already in progress, don't attempt to send it
if (existingRunner.isExecuting) {
return;
}
// If the query is not in progress, we can reuse the query runner
queryRunner = existingRunner;
info.selection = [];
info.selectionSnippet = undefined;
} else {
// We do not have a query runner for this editor, so create a new one
// and map it to the results uri
info = this.initQueryRunner(uri);
queryRunner = info.queryRunner;
}
info.queryInput = queryInput;
if (types.isString(selection)) {
// Run the query string in this case
if (selection.length < selectionSnippetMaxLen) {
info.selectionSnippet = selection;
} else {
info.selectionSnippet = selection.substring(0, selectionSnippetMaxLen - 3) + '...';
}
queryRunner.runQuery(selection, runOptions);
} else if (runCurrentStatement) {
queryRunner.runQueryStatement(selection);
} else {
queryRunner.runQuery(selection, runOptions);
}
}
private initQueryRunner(uri: string): QueryInfo {
let queryRunner = this._instantiationService.createInstance(QueryRunner, uri);
let info = new QueryInfo();
queryRunner.addListener(QREvents.RESULT_SET, e => {
this._fireQueryEvent(uri, 'resultSet', e);
});
queryRunner.addListener(QREvents.BATCH_START, b => {
let link = undefined;
let messageText = LocalizedConstants.runQueryBatchStartMessage;
if (b.selection) {
if (info.selectionSnippet) {
// This indicates it's a query string. Do not include line information since it'll be inaccurate, but show some of the
// executed query text
messageText = nls.localize('runQueryStringBatchStartMessage', 'Started executing query "{0}"', info.selectionSnippet);
} else {
link = {
text: strings.format(LocalizedConstants.runQueryBatchStartLine, b.selection.startLine + 1)
};
}
}
let message = {
message: messageText,
batchId: b.id,
isError: false,
time: new Date().toLocaleTimeString(),
link: link
};
this._fireQueryEvent(uri, 'message', message);
info.selection.push(this._validateSelection(b.selection));
});
queryRunner.addListener(QREvents.MESSAGE, m => {
this._fireQueryEvent(uri, 'message', m);
});
queryRunner.addListener(QREvents.COMPLETE, totalMilliseconds => {
this._onRunQueryComplete.fire(uri);
this._fireQueryEvent(uri, 'complete', totalMilliseconds);
});
queryRunner.addListener(QREvents.START, () => {
this._onRunQueryStart.fire(uri);
this._fireQueryEvent(uri, 'start');
});
info.queryRunner = queryRunner;
info.dataService = this._instantiationService.createInstance(DataService, uri);
this._queryInfoMap.set(uri, info);
return info;
}
public cancelQuery(input: QueryRunner | string): void {
let queryRunner: QueryRunner;
if (typeof input === 'string') {
if (this._queryInfoMap.has(input)) {
queryRunner = this._getQueryInfo(input).queryRunner;
}
} else {
queryRunner = input;
}
if (queryRunner === undefined || !queryRunner.isExecuting) {
// TODO: Cannot cancel query as no query is running.
return;
}
// Switch the spinner to canceling, which will be reset when the query execute sends back its completed event
// TODO indicate on the status bar that the query is being canceled
// Cancel the query
queryRunner.cancelQuery().then(success => undefined, error => {
// On error, show error message and notify that the query is complete so that buttons and other status indicators
// can be correct
this._notificationService.notify({
severity: Severity.Error,
message: strings.format(LocalizedConstants.msgCancelQueryFailed, error)
});
this._fireQueryEvent(queryRunner.uri, 'complete', 0);
});
}
public disposeQuery(ownerUri: string): void {
// Get existing query runner
let queryRunner = this.internalGetQueryRunner(ownerUri);
if (queryRunner) {
queryRunner.disposeQuery();
}
// remove our info map
if (this._queryInfoMap.has(ownerUri)) {
this._queryInfoMap.delete(ownerUri);
}
}
// EDIT DATA METHODS /////////////////////////////////////////////////////
initializeEdit(ownerUri: string, schemaName: string, objectName: string, objectType: string, rowLimit: number, queryString: string): void {
// Reuse existing query runner if it exists
let queryRunner: QueryRunner;
let info: QueryInfo;
if (this._queryInfoMap.has(ownerUri)) {
info = this._getQueryInfo(ownerUri);
let existingRunner: QueryRunner = info.queryRunner;
// If the initialization is already in progress
if (existingRunner.isExecuting) {
return;
}
queryRunner = existingRunner;
} else {
info = new QueryInfo();
// We do not have a query runner for this editor, so create a new one
// and map it to the results uri
queryRunner = this._instantiationService.createInstance(QueryRunner, ownerUri);
const resultSetEventType = 'resultSet';
queryRunner.addListener(QREvents.RESULT_SET, resultSet => {
this._fireQueryEvent(ownerUri, resultSetEventType, resultSet);
});
queryRunner.onResultSetUpdate(resultSetSummary => {
this._fireQueryEvent(ownerUri, resultSetEventType, resultSetSummary);
});
queryRunner.addListener(QREvents.BATCH_START, batch => {
let link = undefined;
let messageText = LocalizedConstants.runQueryBatchStartMessage;
if (batch.selection) {
if (info.selectionSnippet) {
// This indicates it's a query string. Do not include line information since it'll be inaccurate, but show some of the
// executed query text
messageText = nls.localize('runQueryStringBatchStartMessage', 'Started executing query "{0}"', info.selectionSnippet);
} else {
link = {
text: strings.format(LocalizedConstants.runQueryBatchStartLine, batch.selection.startLine + 1)
};
}
}
let message = {
message: messageText,
batchId: batch.id,
isError: false,
time: new Date().toLocaleTimeString(),
link: link
};
this._fireQueryEvent(ownerUri, 'message', message);
});
queryRunner.addListener(QREvents.MESSAGE, message => {
this._fireQueryEvent(ownerUri, 'message', message);
});
queryRunner.addListener(QREvents.COMPLETE, totalMilliseconds => {
this._onRunQueryComplete.fire(ownerUri);
this._fireQueryEvent(ownerUri, 'complete', totalMilliseconds);
});
queryRunner.addListener(QREvents.START, () => {
this._onRunQueryStart.fire(ownerUri);
this._fireQueryEvent(ownerUri, 'start');
});
queryRunner.addListener(QREvents.EDIT_SESSION_READY, e => {
this._onEditSessionReady.fire(e);
this._fireQueryEvent(e.ownerUri, 'editSessionReady');
});
info.queryRunner = queryRunner;
info.dataService = this._instantiationService.createInstance(DataService, ownerUri);
this._queryInfoMap.set(ownerUri, info);
}
if (queryString) {
if (queryString.length < selectionSnippetMaxLen) {
info.selectionSnippet = queryString;
} else {
info.selectionSnippet = queryString.substring(0, selectionSnippetMaxLen - 3) + '...';
}
}
queryRunner.initializeEdit(ownerUri, schemaName, objectName, objectType, rowLimit, queryString);
}
public cancelInitializeEdit(input: QueryRunner | string): void {
// TODO: Implement query cancellation service
}
public disposeEdit(ownerUri: string): Thenable<void> {
// Get existing query runner
let queryRunner = this.internalGetQueryRunner(ownerUri);
if (queryRunner) {
return queryRunner.disposeEdit(ownerUri);
}
return TPromise.as(null);
}
public updateCell(ownerUri: string, rowId: number, columnId: number, newValue: string): Thenable<sqlops.EditUpdateCellResult> {
// Get existing query runner
let queryRunner = this.internalGetQueryRunner(ownerUri);
if (queryRunner) {
return queryRunner.updateCell(ownerUri, rowId, columnId, newValue).then((result) => result, error => {
this._notificationService.notify({
severity: Severity.Error,
message: nls.localize('updateCellFailed', 'Update cell failed: ') + error.message
});
return Promise.reject(error);
});
}
return TPromise.as(null);
}
public commitEdit(ownerUri): Thenable<void> {
// Get existing query runner
let queryRunner = this.internalGetQueryRunner(ownerUri);
if (queryRunner) {
return queryRunner.commitEdit(ownerUri).then(() => { }, error => {
this._notificationService.notify({
severity: Severity.Error,
message: nls.localize('commitEditFailed', 'Commit row failed: ') + error.message
});
return Promise.reject(error);
});
}
return TPromise.as(null);
}
public createRow(ownerUri: string): Thenable<sqlops.EditCreateRowResult> {
// Get existing query runner
let queryRunner = this.internalGetQueryRunner(ownerUri);
if (queryRunner) {
return queryRunner.createRow(ownerUri);
}
return TPromise.as(null);
}
public deleteRow(ownerUri: string, rowId: number): Thenable<void> {
// Get existing query runner
let queryRunner = this.internalGetQueryRunner(ownerUri);
if (queryRunner) {
return queryRunner.deleteRow(ownerUri, rowId);
}
return TPromise.as(null);
}
public revertCell(ownerUri: string, rowId: number, columnId: number): Thenable<sqlops.EditRevertCellResult> {
// Get existing query runner
let queryRunner = this.internalGetQueryRunner(ownerUri);
if (queryRunner) {
return queryRunner.revertCell(ownerUri, rowId, columnId);
}
return TPromise.as(null);
}
public revertRow(ownerUri: string, rowId: number): Thenable<void> {
// Get existing query runner
let queryRunner = this.internalGetQueryRunner(ownerUri);
if (queryRunner) {
return queryRunner.revertRow(ownerUri, rowId);
}
return TPromise.as(null);
}
public getQueryRunner(ownerUri): QueryRunner {
let queryRunner: QueryRunner = undefined;
if (this._queryInfoMap.has(ownerUri)) {
queryRunner = this._getQueryInfo(ownerUri).queryRunner;
}
// return undefined if not found or is already executing
return queryRunner;
}
// PRIVATE METHODS //////////////////////////////////////////////////////
private internalGetQueryRunner(ownerUri): QueryRunner {
let queryRunner: QueryRunner = undefined;
if (this._queryInfoMap.has(ownerUri)) {
let existingRunner = this._getQueryInfo(ownerUri).queryRunner;
// If the query is not already executing then set it up
if (!existingRunner.isExecuting) {
queryRunner = this._getQueryInfo(ownerUri).queryRunner;
}
}
// return undefined if not found or is already executing
return queryRunner;
}
private _fireGridContentEvent(uri: string, type: string): void {
let info: QueryInfo = this._getQueryInfo(uri);
if (info && info.dataServiceReady) {
let service: DataService = this.getDataService(uri);
if (service) {
// There is no need to queue up these events like there is for the query events because
// if the DataService is not yet ready there will be no grid content to update
service.gridContentObserver.next(type);
}
}
}
private _fireQueryEvent(uri: string, type: string, data?: any) {
let info: QueryInfo = this._getQueryInfo(uri);
if (info.dataServiceReady) {
let service: DataService = this.getDataService(uri);
service.queryEventObserver.next({
type: type,
data: data
});
} else {
let queueItem: QueryEvent = { type: type, data: data };
info.queryEventQueue.push(queueItem);
}
}
private _sendQueuedEvents(uri: string): void {
let info: QueryInfo = this._getQueryInfo(uri);
while (info.queryEventQueue.length > 0) {
let event: QueryEvent = info.queryEventQueue.shift();
this._fireQueryEvent(uri, event.type, event.data);
}
}
public _getQueryInfo(uri: string): QueryInfo {
return this._queryInfoMap.get(uri);
}
// TODO remove this funciton and its usages when #821 in vscode-mssql is fixed and
// the SqlToolsService version is updated in this repo - coquagli 4/19/2017
private _validateSelection(selection: sqlops.ISelectionData): sqlops.ISelectionData {
if (!selection) {
selection = <sqlops.ISelectionData>{};
}
selection.endColumn = selection ? Math.max(0, selection.endColumn) : 0;
selection.endLine = selection ? Math.max(0, selection.endLine) : 0;
selection.startColumn = selection ? Math.max(0, selection.startColumn) : 0;
selection.startLine = selection ? Math.max(0, selection.startLine) : 0;
return selection;
}
}

View File

@@ -0,0 +1,664 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import * as Constants from 'sql/parts/query/common/constants';
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
import { IQueryManagementService } from 'sql/platform/query/common/queryManagement';
import * as Utils from 'sql/platform/connection/common/utils';
import { SaveFormat } from 'sql/parts/grid/common/interfaces';
import { Deferred } from 'sql/base/common/promise';
import Severity from 'vs/base/common/severity';
import * as nls from 'vs/nls';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import * as types from 'vs/base/common/types';
import { EventEmitter } from 'sql/base/common/eventEmitter';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { Emitter, Event } from 'vs/base/common/event';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ResultSerializer } from 'sql/platform/node/resultSerializer';
import { TPromise } from 'vs/base/common/winjs.base';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
export interface IEditSessionReadyEvent {
ownerUri: string;
success: boolean;
message: string;
}
export const enum EventType {
START = 'start',
COMPLETE = 'complete',
MESSAGE = 'message',
BATCH_START = 'batchStart',
BATCH_COMPLETE = 'batchComplete',
RESULT_SET = 'resultSet',
EDIT_SESSION_READY = 'editSessionReady'
}
export interface IEventType {
start: void;
complete: string;
message: sqlops.IResultMessage;
batchStart: sqlops.BatchSummary;
batchComplete: sqlops.BatchSummary;
resultSet: sqlops.ResultSetSummary;
editSessionReady: IEditSessionReadyEvent;
}
export interface IGridMessage extends sqlops.IResultMessage {
selection: sqlops.ISelectionData;
}
/*
* Query Runner class which handles running a query, reports the results to the content manager,
* and handles getting more rows from the service layer and disposing when the content is closed.
*/
export default class QueryRunner extends Disposable {
// MEMBER VARIABLES ////////////////////////////////////////////////////
private _resultLineOffset: number;
private _totalElapsedMilliseconds: number = 0;
private _isExecuting: boolean = false;
private _hasCompleted: boolean = false;
private _batchSets: sqlops.BatchSummary[] = [];
private _messages: sqlops.IResultMessage[] = [];
private _eventEmitter = new EventEmitter();
private _isQueryPlan: boolean;
public get isQueryPlan(): boolean { return this._isQueryPlan; }
private _planXml = new Deferred<string>();
public get planXml(): Thenable<string> { return this._planXml.promise; }
private _onMessage = this._register(new Emitter<sqlops.IResultMessage>());
public readonly onMessage = this._onMessage.event;
private _onResultSet = this._register(new Emitter<sqlops.ResultSetSummary>());
public readonly onResultSet = this._onResultSet.event;
private _onResultSetUpdate = this._register(new Emitter<sqlops.ResultSetSummary>());
public readonly onResultSetUpdate = this._onResultSetUpdate.event;
private _onQueryStart = this._register(new Emitter<void>());
public readonly onQueryStart: Event<void> = this._onQueryStart.event;
private _onQueryEnd = this._register(new Emitter<string>());
public readonly onQueryEnd: Event<string> = this._onQueryEnd.event;
private _onBatchStart = this._register(new Emitter<sqlops.BatchSummary>());
public readonly onBatchStart: Event<sqlops.BatchSummary> = this._onBatchStart.event;
private _onBatchEnd = this._register(new Emitter<sqlops.BatchSummary>());
public readonly onBatchEnd: Event<sqlops.BatchSummary> = this._onBatchEnd.event;
private _queryStartTime: Date;
public get queryStartTime(): Date {
return this._queryStartTime;
}
private _queryEndTime: Date;
public get queryEndTime(): Date {
return this._queryEndTime;
}
// CONSTRUCTOR /////////////////////////////////////////////////////////
constructor(
public uri: string,
@IQueryManagementService private _queryManagementService: IQueryManagementService,
@INotificationService private _notificationService: INotificationService,
@IConfigurationService private _configurationService: IConfigurationService,
@IClipboardService private _clipboardService: IClipboardService,
@IInstantiationService private instantiationService: IInstantiationService
) {
super();
}
get isExecuting(): boolean {
return this._isExecuting;
}
get hasCompleted(): boolean {
return this._hasCompleted;
}
/**
* For public use only, for private use, directly access the member
*/
public get batchSets(): sqlops.BatchSummary[] {
return this._batchSets.slice(0);
}
/**
* For public use only, for private use, directly access the member
*/
public get messages(): sqlops.IResultMessage[] {
return this._messages.slice(0);
}
// PUBLIC METHODS ======================================================
public addListener<K extends keyof IEventType>(event: K, f: (e: IEventType[K]) => void): IDisposable {
return this._eventEmitter.addListener(event, f);
}
/**
* Cancels the running query, if there is one
*/
public cancelQuery(): Thenable<sqlops.QueryCancelResult> {
return this._queryManagementService.cancelQuery(this.uri);
}
/**
* Runs the query with the provided query
* @param input Query string to execute
*/
public runQuery(input: string, runOptions?: sqlops.ExecutionPlanOptions): Thenable<void>;
/**
* Runs the query by pulling the query from the document using the provided selection data
* @param input selection data
*/
public runQuery(input: sqlops.ISelectionData, runOptions?: sqlops.ExecutionPlanOptions): Thenable<void>;
public runQuery(input, runOptions?: sqlops.ExecutionPlanOptions): Thenable<void> {
return this.doRunQuery(input, false, runOptions);
}
/**
* Runs the current SQL statement by pulling the query from the document using the provided selection data
* @param input selection data
*/
public runQueryStatement(input: sqlops.ISelectionData): Thenable<void> {
return this.doRunQuery(input, true);
}
/**
* Implementation that runs the query with the provided query
* @param input Query string to execute
*/
private doRunQuery(input: string, runCurrentStatement: boolean, runOptions?: sqlops.ExecutionPlanOptions): Thenable<void>;
private doRunQuery(input: sqlops.ISelectionData, runCurrentStatement: boolean, runOptions?: sqlops.ExecutionPlanOptions): Thenable<void>;
private doRunQuery(input, runCurrentStatement: boolean, runOptions?: sqlops.ExecutionPlanOptions): Thenable<void> {
if (this.isExecuting) {
return TPromise.as(undefined);
}
this._planXml = new Deferred<string>();
this._batchSets = [];
this._hasCompleted = false;
this._queryStartTime = undefined;
this._queryEndTime = undefined;
this._messages = [];
if (types.isObject(input) || types.isUndefinedOrNull(input)) {
// Update internal state to show that we're executing the query
this._resultLineOffset = input ? input.startLine : 0;
this._isExecuting = true;
this._totalElapsedMilliseconds = 0;
// TODO issue #228 add statusview callbacks here
if (runOptions && (runOptions.displayActualQueryPlan || runOptions.displayEstimatedQueryPlan)) {
this._isQueryPlan = true;
} else {
this._isQueryPlan = false;
}
// Send the request to execute the query
return runCurrentStatement
? this._queryManagementService.runQueryStatement(this.uri, input.startLine, input.startColumn).then(() => this.handleSuccessRunQueryResult(), e => this.handleFailureRunQueryResult(e))
: this._queryManagementService.runQuery(this.uri, input, runOptions).then(() => this.handleSuccessRunQueryResult(), e => this.handleFailureRunQueryResult(e));
} else if (types.isString(input)) {
// Update internal state to show that we're executing the query
this._isExecuting = true;
this._totalElapsedMilliseconds = 0;
return this._queryManagementService.runQueryString(this.uri, input).then(() => this.handleSuccessRunQueryResult(), e => this.handleFailureRunQueryResult(e));
} else {
return Promise.reject('Unknown input');
}
}
private handleSuccessRunQueryResult() {
// this isn't exact, but its the best we can do
this._queryStartTime = new Date();
// The query has started, so lets fire up the result pane
this._onQueryStart.fire();
this._eventEmitter.emit(EventType.START);
this._queryManagementService.registerRunner(this, this.uri);
}
private handleFailureRunQueryResult(error: any) {
// Attempting to launch the query failed, show the error message
const eol = this.getEolString();
let message = nls.localize('query.ExecutionFailedError', 'Execution failed due to an unexpected error: {0}\t{1}', eol, error);
this.handleMessage(<sqlops.QueryExecuteMessageParams>{
ownerUri: this.uri,
message: {
isError: true,
message: message
}
});
this.handleQueryComplete(<sqlops.QueryExecuteCompleteNotificationResult>{ ownerUri: this.uri });
}
/**
* Handle a QueryComplete from the service layer
*/
public handleQueryComplete(result: sqlops.QueryExecuteCompleteNotificationResult): void {
// this also isn't exact but its the best we can do
this._queryEndTime = new Date();
// Store the batch sets we got back as a source of "truth"
this._isExecuting = false;
this._hasCompleted = true;
this._batchSets = result.batchSummaries ? result.batchSummaries : [];
this._batchSets.map(batch => {
if (batch.selection) {
batch.selection.startLine = batch.selection.startLine + this._resultLineOffset;
batch.selection.endLine = batch.selection.endLine + this._resultLineOffset;
}
});
let timeStamp = Utils.parseNumAsTimeString(this._totalElapsedMilliseconds);
this._eventEmitter.emit(EventType.COMPLETE, timeStamp);
// We're done with this query so shut down any waiting mechanisms
let message = {
message: nls.localize('query.message.executionTime', 'Total execution time: {0}', timeStamp),
isError: false,
time: undefined
};
this._messages.push(message);
this._onQueryEnd.fire(timeStamp);
this._onMessage.fire(message);
}
/**
* Handle a BatchStart from the service layer
*/
public handleBatchStart(result: sqlops.QueryExecuteBatchNotificationParams): void {
let batch = result.batchSummary;
// Recalculate the start and end lines, relative to the result line offset
if (batch.selection) {
batch.selection.startLine += this._resultLineOffset;
batch.selection.endLine += this._resultLineOffset;
}
// Set the result sets as an empty array so that as result sets complete we can add to the list
batch.resultSetSummaries = [];
// Store the batch
this._batchSets[batch.id] = batch;
let message = {
// account for index by 1
message: nls.localize('query.message.startQuery', 'Started executing query at Line {0}', batch.selection.startLine + 1),
time: new Date(batch.executionStart).toLocaleTimeString(),
selection: batch.selection,
isError: false
};
this._messages.push(message);
this._eventEmitter.emit(EventType.BATCH_START, batch);
this._onMessage.fire(message);
this._onBatchStart.fire(batch);
}
/**
* Handle a BatchComplete from the service layer
*/
public handleBatchComplete(result: sqlops.QueryExecuteBatchNotificationParams): void {
let batch: sqlops.BatchSummary = result.batchSummary;
// Store the batch again to get the rest of the data
this._batchSets[batch.id] = batch;
let executionTime = <number>(Utils.parseTimeString(batch.executionElapsed) || 0);
this._totalElapsedMilliseconds += executionTime;
if (executionTime > 0) {
// send a time message in the format used for query complete
this.sendBatchTimeMessage(batch.id, Utils.parseNumAsTimeString(executionTime));
}
this._eventEmitter.emit(EventType.BATCH_COMPLETE, batch);
this._onBatchEnd.fire(batch);
}
/**
* Handle a ResultSetComplete from the service layer
*/
public handleResultSetAvailable(result: sqlops.QueryExecuteResultSetNotificationParams): void {
if (result && result.resultSetSummary) {
let resultSet = result.resultSetSummary;
let batchSet: sqlops.BatchSummary;
if (!resultSet.batchId) {
// Missing the batchId. In this case, default to always using the first batch in the list
// or create one in the case the DMP extension didn't obey the contract perfectly
if (this._batchSets.length > 0) {
batchSet = this._batchSets[0];
} else {
batchSet = <sqlops.BatchSummary>{
id: 0,
selection: undefined,
hasError: false,
resultSetSummaries: []
};
this._batchSets[0] = batchSet;
}
} else {
batchSet = this._batchSets[resultSet.batchId];
}
// handle getting queryPlanxml if we need too
if (this.isQueryPlan) {
// check if this result has show plan, this needs work, it won't work for any other provider
let hasShowPlan = !!result.resultSetSummary.columnInfo.find(e => e.columnName === 'Microsoft SQL Server 2005 XML Showplan');
if (hasShowPlan) {
this.getQueryRows(0, 1, result.resultSetSummary.batchId, result.resultSetSummary.id).then(e => this._planXml.resolve(e.resultSubset.rows[0][0].displayValue));
}
}
// we will just ignore the set if we already have it
// ideally this should never happen
if (batchSet && !batchSet.resultSetSummaries[resultSet.id]) {
// Store the result set in the batch and emit that a result set has completed
batchSet.resultSetSummaries[resultSet.id] = resultSet;
this._eventEmitter.emit(EventType.RESULT_SET, resultSet);
this._onResultSet.fire(resultSet);
}
}
}
public handleResultSetUpdated(result: sqlops.QueryExecuteResultSetNotificationParams): void {
if (result && result.resultSetSummary) {
let resultSet = result.resultSetSummary;
let batchSet: sqlops.BatchSummary;
batchSet = this._batchSets[resultSet.batchId];
// handle getting queryPlanxml if we need too
if (this.isQueryPlan) {
// check if this result has show plan, this needs work, it won't work for any other provider
let hasShowPlan = !!result.resultSetSummary.columnInfo.find(e => e.columnName === 'Microsoft SQL Server 2005 XML Showplan');
if (hasShowPlan) {
this.getQueryRows(0, 1, result.resultSetSummary.batchId, result.resultSetSummary.id).then(e => this._planXml.resolve(e.resultSubset.rows[0][0].displayValue));
}
}
if (batchSet) {
// Store the result set in the batch and emit that a result set has completed
batchSet.resultSetSummaries[resultSet.id] = resultSet;
this._onResultSetUpdate.fire(resultSet);
}
}
}
/**
* Handle a Mssage from the service layer
*/
public handleMessage(obj: sqlops.QueryExecuteMessageParams): void {
let message = obj.message;
message.time = new Date(message.time).toLocaleTimeString();
this._messages.push(message);
// Send the message to the results pane
this._eventEmitter.emit(EventType.MESSAGE, message);
this._onMessage.fire(message);
}
/**
* Get more data rows from the current resultSets from the service layer
*/
public getQueryRows(rowStart: number, numberOfRows: number, batchIndex: number, resultSetIndex: number): Thenable<sqlops.QueryExecuteSubsetResult> {
let rowData: sqlops.QueryExecuteSubsetParams = <sqlops.QueryExecuteSubsetParams>{
ownerUri: this.uri,
resultSetIndex: resultSetIndex,
rowsCount: numberOfRows,
rowsStartIndex: rowStart,
batchIndex: batchIndex
};
return this._queryManagementService.getQueryRows(rowData).then(r => r, error => {
// this._notificationService.notify({
// severity: Severity.Error,
// message: nls.localize('query.gettingRowsFailedError', 'Something went wrong getting more rows: {0}', error)
// });
return error;
});
}
/*
* Handle a session ready event for Edit Data
*/
public initializeEdit(ownerUri: string, schemaName: string, objectName: string, objectType: string, rowLimit: number, queryString: string): Thenable<void> {
// Update internal state to show that we're executing the query
this._isExecuting = true;
this._totalElapsedMilliseconds = 0;
// TODO issue #228 add statusview callbacks here
return this._queryManagementService.initializeEdit(ownerUri, schemaName, objectName, objectType, rowLimit, queryString).then(result => {
// The query has started, so lets fire up the result pane
this._eventEmitter.emit(EventType.START);
this._queryManagementService.registerRunner(this, ownerUri);
}, error => {
// Attempting to launch the query failed, show the error message
// TODO issue #228 add statusview callbacks here
this._isExecuting = false;
this._notificationService.notify({
severity: Severity.Error,
message: nls.localize('query.initEditExecutionFailed', 'Init Edit Execution failed: ') + error
});
});
}
/**
* Retrieves a number of rows from an edit session
* @param rowStart The index of the row to start returning (inclusive)
* @param numberOfRows The number of rows to return
*/
public getEditRows(rowStart: number, numberOfRows: number): Thenable<sqlops.EditSubsetResult> {
const self = this;
let rowData: sqlops.EditSubsetParams = {
ownerUri: this.uri,
rowCount: numberOfRows,
rowStartIndex: rowStart
};
return new Promise<sqlops.EditSubsetResult>((resolve, reject) => {
self._queryManagementService.getEditRows(rowData).then(result => {
if (!result.hasOwnProperty('rowCount')) {
let error = `Nothing returned from subset query`;
self._notificationService.notify({
severity: Severity.Error,
message: error
});
reject(error);
}
resolve(result);
}, error => {
// let errorMessage = nls.localize('query.moreRowsFailedError', 'Something went wrong getting more rows:');
// self._notificationService.notify({
// severity: Severity.Error,
// message: `${errorMessage} ${error}`
// });
reject(error);
});
});
}
public handleEditSessionReady(ownerUri: string, success: boolean, message: string): void {
this._eventEmitter.emit(EventType.EDIT_SESSION_READY, { ownerUri, success, message });
}
public updateCell(ownerUri: string, rowId: number, columnId: number, newValue: string): Thenable<sqlops.EditUpdateCellResult> {
return this._queryManagementService.updateCell(ownerUri, rowId, columnId, newValue);
}
public commitEdit(ownerUri): Thenable<void> {
return this._queryManagementService.commitEdit(ownerUri);
}
public createRow(ownerUri: string): Thenable<sqlops.EditCreateRowResult> {
return this._queryManagementService.createRow(ownerUri).then(result => {
return result;
});
}
public deleteRow(ownerUri: string, rowId: number): Thenable<void> {
return this._queryManagementService.deleteRow(ownerUri, rowId);
}
public revertCell(ownerUri: string, rowId: number, columnId: number): Thenable<sqlops.EditRevertCellResult> {
return this._queryManagementService.revertCell(ownerUri, rowId, columnId).then(result => {
return result;
});
}
public revertRow(ownerUri: string, rowId: number): Thenable<void> {
return this._queryManagementService.revertRow(ownerUri, rowId);
}
public disposeEdit(ownerUri: string): Thenable<void> {
return this._queryManagementService.disposeEdit(ownerUri);
}
/**
* Disposes the Query from the service client
*/
public disposeQuery(): void {
this._queryManagementService.disposeQuery(this.uri).then(() => {
this.dispose();
});
}
public dispose() {
this._batchSets = undefined;
super.dispose();
}
get totalElapsedMilliseconds(): number {
return this._totalElapsedMilliseconds;
}
/**
* Sends a copy request
* @param selection The selection range to copy
* @param batchId The batch id of the result to copy from
* @param resultId The result id of the result to copy from
* @param includeHeaders [Optional]: Should column headers be included in the copy selection
*/
copyResults(selection: Slick.Range[], batchId: number, resultId: number, includeHeaders?: boolean): void {
const self = this;
let copyString = '';
const eol = this.getEolString();
// create a mapping of the ranges to get promises
let tasks = selection.map((range, i) => {
return () => {
return self.getQueryRows(range.fromRow, range.toRow - range.fromRow + 1, batchId, resultId).then((result) => {
// If there was a previous selection separate it with a line break. Currently
// when there are multiple selections they are never on the same line
if (i > 0) {
copyString += eol;
}
if (self.shouldIncludeHeaders(includeHeaders)) {
let columnHeaders = self.getColumnHeaders(batchId, resultId, range);
if (columnHeaders !== undefined) {
copyString += columnHeaders.join('\t') + eol;
}
}
// Iterate over the rows to paste into the copy string
for (let rowIndex: number = 0; rowIndex < result.resultSubset.rows.length; rowIndex++) {
let row = result.resultSubset.rows[rowIndex];
let cellObjects = row.slice(range.fromCell, (range.toCell + 1));
// Remove newlines if requested
let cells = self.shouldRemoveNewLines()
? cellObjects.map(x => self.removeNewLines(x.displayValue))
: cellObjects.map(x => x.displayValue);
copyString += cells.join('\t');
if (rowIndex < result.resultSubset.rows.length - 1) {
copyString += eol;
}
}
});
};
});
if (tasks.length > 0) {
let p = tasks[0]();
for (let i = 1; i < tasks.length; i++) {
p = p.then(tasks[i]);
}
p.then(() => {
this._clipboardService.writeText(copyString);
});
}
}
private getEolString(): string {
const { eol } = this._configurationService.getValue<{ eol: string }>('files');
return eol;
}
private shouldIncludeHeaders(includeHeaders: boolean): boolean {
if (includeHeaders !== undefined) {
// Respect the value explicity passed into the method
return includeHeaders;
}
// else get config option from vscode config
includeHeaders = WorkbenchUtils.getSqlConfigValue<boolean>(this._configurationService, Constants.copyIncludeHeaders);
return !!includeHeaders;
}
private shouldRemoveNewLines(): boolean {
// get config copyRemoveNewLine option from vscode config
let removeNewLines: boolean = WorkbenchUtils.getSqlConfigValue<boolean>(this._configurationService, Constants.configCopyRemoveNewLine);
return !!removeNewLines;
}
private getColumnHeaders(batchId: number, resultId: number, range: Slick.Range): string[] {
let headers: string[] = undefined;
let batchSummary: sqlops.BatchSummary = this._batchSets[batchId];
if (batchSummary !== undefined) {
let resultSetSummary = batchSummary.resultSetSummaries[resultId];
headers = resultSetSummary.columnInfo.slice(range.fromCell, range.toCell + 1).map((info, i) => {
return info.columnName;
});
}
return headers;
}
private removeNewLines(inputString: string): string {
// This regex removes all newlines in all OS types
// Windows(CRLF): \r\n
// Linux(LF)/Modern MacOS: \n
// Old MacOs: \r
if (types.isUndefinedOrNull(inputString)) {
return 'null';
}
let outputString: string = inputString.replace(/(\r\n|\n|\r)/gm, '');
return outputString;
}
private sendBatchTimeMessage(batchId: number, executionTime: string): void {
// get config copyRemoveNewLine option from vscode config
let showBatchTime: boolean = WorkbenchUtils.getSqlConfigValue<boolean>(this._configurationService, Constants.configShowBatchTime);
if (showBatchTime) {
let message: sqlops.IResultMessage = {
batchId: batchId,
message: nls.localize('elapsedBatchTime', 'Batch execution time: {0}', executionTime),
time: undefined,
isError: false
};
this._messages.push(message);
// Send the message to the results pane
this._onMessage.fire(message);
}
}
public serializeResults(batchId: number, resultSetId: number, format: SaveFormat, selection: Slick.Range[]) {
return this.instantiationService.createInstance(ResultSerializer).saveResults(this.uri, { selection, format, batchIndex: batchId, resultSetNumber: resultSetId });
}
}