diff --git a/src/sql/platform/connection/common/connectionManagement.ts b/src/sql/platform/connection/common/connectionManagement.ts index 519eb52dae..7992ba2c39 100644 --- a/src/sql/platform/connection/common/connectionManagement.ts +++ b/src/sql/platform/connection/common/connectionManagement.ts @@ -12,6 +12,28 @@ import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { ConnectionManagementInfo } from 'sql/platform/connection/common/connectionManagementInfo'; import { ConnectionProviderProperties } from 'sql/platform/capabilities/common/capabilitiesService'; +/** + * A range in the editor. This interface is suitable for serialization. + */ +export interface IRange { + /** + * Line number on which the range starts (starts at 1). + */ + readonly startLineNumber: number; + /** + * Column on which the range starts in line `startLineNumber` (starts at 1). + */ + readonly startColumn: number; + /** + * Line number on which the range ends. + */ + readonly endLineNumber: number; + /** + * Column on which the range ends in line `endLineNumber`. + */ + readonly endColumn: number; +} + /** * Options for the actions that could happen after connecting is complete */ @@ -302,7 +324,7 @@ export interface INewConnectionParams { connectionType: ConnectionType; input?: IConnectableInput; runQueryOnCompletion?: RunQueryOnConnectionMode; - querySelection?: azdata.ISelectionData; + queryRange?: IRange; showDashboard?: boolean; providers?: string[]; isEditConnection?: boolean; diff --git a/src/sql/workbench/common/editor/query/queryEditorInput.ts b/src/sql/workbench/common/editor/query/queryEditorInput.ts index 791482d79f..2352a53acd 100644 --- a/src/sql/workbench/common/editor/query/queryEditorInput.ts +++ b/src/sql/workbench/common/editor/query/queryEditorInput.ts @@ -14,8 +14,9 @@ import { IConnectionManagementService, IConnectableInput, INewConnectionParams, import { QueryResultsInput } from 'sql/workbench/common/editor/query/queryResultsInput'; import { IQueryModelService } from 'sql/workbench/services/query/common/queryModel'; -import { ISelectionData, ExecutionPlanOptions } from 'azdata'; +import { ExecutionPlanOptions } from 'azdata'; import { startsWith } from 'vs/base/common/strings'; +import { IRange } from 'vs/editor/common/core/range'; const MAX_SIZE = 13; @@ -229,13 +230,13 @@ export abstract class QueryEditorInput extends EditorInput implements IConnectab } // State update funtions - public runQuery(selection?: ISelectionData, executePlanOptions?: ExecutionPlanOptions): void { - this.queryModelService.runQuery(this.uri, selection, executePlanOptions); + public runQuery(range?: IRange, executePlanOptions?: ExecutionPlanOptions): void { + this.queryModelService.runQuery(this.uri, range, executePlanOptions); this.state.executing = true; } - public runQueryStatement(selection?: ISelectionData): void { - this.queryModelService.runQueryStatement(this.uri, selection); + public runQueryStatement(range?: IRange): void { + this.queryModelService.runQueryStatement(this.uri, range); this.state.executing = true; } @@ -269,15 +270,15 @@ export abstract class QueryEditorInput extends EditorInput implements IConnectab let isRunningQuery = this.queryModelService.isRunningQuery(this.uri); if (!isRunningQuery && params && params.runQueryOnCompletion) { - let selection: ISelectionData | undefined = params ? params.querySelection : undefined; + let range: IRange | undefined = params ? params.queryRange : undefined; if (params.runQueryOnCompletion === RunQueryOnConnectionMode.executeCurrentQuery) { - this.runQueryStatement(selection); + this.runQueryStatement(range); } else if (params.runQueryOnCompletion === RunQueryOnConnectionMode.executeQuery) { - this.runQuery(selection); + this.runQuery(range); } else if (params.runQueryOnCompletion === RunQueryOnConnectionMode.estimatedQueryPlan) { - this.runQuery(selection, { displayEstimatedQueryPlan: true }); + this.runQuery(range, { displayEstimatedQueryPlan: true }); } else if (params.runQueryOnCompletion === RunQueryOnConnectionMode.actualQueryPlan) { - this.runQuery(selection, { displayActualQueryPlan: true }); + this.runQuery(range, { displayActualQueryPlan: true }); } } this._onDidChangeLabel.fire(); diff --git a/src/sql/workbench/contrib/charts/browser/chartView.ts b/src/sql/workbench/contrib/charts/browser/chartView.ts index 4fa8e78b62..5ccca7d4c8 100644 --- a/src/sql/workbench/contrib/charts/browser/chartView.ts +++ b/src/sql/workbench/contrib/charts/browser/chartView.ts @@ -8,6 +8,7 @@ import 'vs/css!./media/chartView'; import { IPanelView } from 'sql/base/browser/ui/panel/panel'; import { Insight } from './insight'; import QueryRunner from 'sql/workbench/services/query/common/queryRunner'; +import { ICellValue } from 'sql/workbench/services/query/common/query'; import { ChartOptions, IChartOption, ControlType } from './chartOptions'; import { Extensions, IInsightRegistry, IInsightData } from 'sql/platform/dashboard/browser/insightRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -28,7 +29,6 @@ import { ChartState } from 'sql/workbench/common/editor/query/chartState'; import * as nls from 'vs/nls'; import { find } from 'vs/base/common/arrays'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { DbCellValue } from 'azdata'; import { Event, Emitter } from 'vs/base/common/event'; const insightRegistry = Registry.as(Extensions.InsightContribution); @@ -213,7 +213,7 @@ export class ChartView extends Disposable implements IPanelView { this.shouldGraph(); } - public setData(rows: DbCellValue[][], columns: string[]): void { + public setData(rows: ICellValue[][], columns: string[]): void { if (!rows) { this._data = { columns: [], rows: [] }; this._notificationService.error(nls.localize('charting.failedToGetRows', "Failed to get rows for the dataset to chart.")); @@ -238,7 +238,7 @@ export class ChartView extends Disposable implements IPanelView { let summary = batch.resultSetSummaries[this._currentData.resultId]; if (summary) { this._queryRunner.getQueryRows(0, summary.rowCount, this._currentData.batchId, this._currentData.resultId).then(d => { - let rows = d.resultSubset.rows; + let rows = d.rows; let columns = summary.columnInfo.map(c => c.columnName); this.setData(rows, columns); }); diff --git a/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts b/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts index c45797740a..552502763e 100644 --- a/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts @@ -14,6 +14,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IDataResource } from 'sql/workbench/services/notebook/browser/sql/sqlSessionManager'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; import { getEolString, shouldIncludeHeaders, shouldRemoveNewLines } from 'sql/workbench/services/query/common/queryRunner'; +import { ICellValue, ResultSetSummary, ResultSetSubset } from 'sql/workbench/services/query/common/query'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { attachTableStyler } from 'sql/platform/theme/common/styler'; @@ -215,7 +216,7 @@ class DataResourceTable extends GridTableBase { gridDataProvider.getRowData(0, rowCount).then(result => { let range = new Slick.Range(0, 0, rowCount - 1, columnCount - 1); let columns = gridDataProvider.getColumnHeaders(range); - this._chart.setData(result.resultSubset.rows, columns); + this._chart.setData(result.rows, columns); }); } @@ -226,9 +227,9 @@ class DataResourceTable extends GridTableBase { } class DataResourceDataProvider implements IGridDataProvider { - private rows: azdata.DbCellValue[][]; + private rows: ICellValue[][]; constructor(source: IDataResource, - private resultSet: azdata.ResultSetSummary, + private resultSet: ResultSetSummary, private documentUri: string, @INotificationService private _notificationService: INotificationService, @IClipboardService private _clipboardService: IClipboardService, @@ -256,17 +257,14 @@ class DataResourceDataProvider implements IGridDataProvider { }); } - getRowData(rowStart: number, numberOfRows: number): Thenable { + getRowData(rowStart: number, numberOfRows: number): Thenable { let rowEnd = rowStart + numberOfRows; if (rowEnd > this.rows.length) { rowEnd = this.rows.length; } - let resultSubset: azdata.QueryExecuteSubsetResult = { - message: undefined, - resultSubset: { - rowCount: rowEnd - rowStart, - rows: this.rows.slice(rowStart, rowEnd) - } + let resultSubset: ResultSetSubset = { + rowCount: rowEnd - rowStart, + rows: this.rows.slice(rowStart, rowEnd) }; return Promise.resolve(resultSubset); } @@ -326,7 +324,7 @@ class DataResourceDataProvider implements IGridDataProvider { maxRow = singleSelection.toRow + 1; columns = columns.slice(singleSelection.fromCell, singleSelection.toCell + 1); } - let getRows: ((index: number, rowCount: number) => azdata.DbCellValue[][]) = (index, rowCount) => { + let getRows: ((index: number, rowCount: number) => ICellValue[][]) = (index, rowCount) => { // Offset for selections by adding the selection startRow to the index index = index + minRow; if (rowLength === 0 || index < 0 || index >= maxRow) { diff --git a/src/sql/workbench/contrib/query/browser/gridPanel.ts b/src/sql/workbench/contrib/query/browser/gridPanel.ts index 3fb394b3ad..a1d566f94a 100644 --- a/src/sql/workbench/contrib/query/browser/gridPanel.ts +++ b/src/sql/workbench/contrib/query/browser/gridPanel.ts @@ -7,6 +7,7 @@ import 'vs/css!./media/gridPanel'; import { attachTableStyler } from 'sql/platform/theme/common/styler'; import QueryRunner, { QueryGridDataProvider } from 'sql/workbench/services/query/common/queryRunner'; +import { ResultSetSummary, IColumn } from 'sql/workbench/services/query/common/query'; import { VirtualizedCollection, AsyncDataProvider } from 'sql/base/browser/ui/table/asyncDataView'; import { Table } from 'sql/base/browser/ui/table/table'; import { ScrollableSplitView, IView } from 'sql/base/browser/ui/scrollableSplitview/scrollableSplitview'; @@ -21,8 +22,6 @@ import { CopyKeybind } from 'sql/base/browser/ui/table/plugins/copyKeybind.plugi import { AdditionalKeyBindings } from 'sql/base/browser/ui/table/plugins/additionalKeyBindings.plugin'; import { ITableStyles, ITableMouseEvent } from 'sql/base/browser/ui/table/interfaces'; -import * as azdata from 'azdata'; - import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -123,7 +122,7 @@ export class GridPanel extends Disposable { } this.reset(); })); - this.addResultSet(this.runner.batchSets.reduce((p, e) => { + this.addResultSet(this.runner.batchSets.reduce((p, e) => { if (this.configurationService.getValue('sql.results.streaming')) { p = p.concat(e.resultSetSummaries); } else { @@ -141,8 +140,8 @@ export class GridPanel extends Disposable { this.splitView.setScrollPosition(this.state.scrollPosition); } - private onResultSet(resultSet: azdata.ResultSetSummary | azdata.ResultSetSummary[]) { - let resultsToAdd: azdata.ResultSetSummary[]; + private onResultSet(resultSet: ResultSetSummary | ResultSetSummary[]) { + let resultsToAdd: ResultSetSummary[]; if (!Array.isArray(resultSet)) { resultsToAdd = [resultSet]; } else { @@ -170,8 +169,8 @@ export class GridPanel extends Disposable { } } - private updateResultSet(resultSet: azdata.ResultSetSummary | azdata.ResultSetSummary[]) { - let resultsToUpdate: azdata.ResultSetSummary[]; + private updateResultSet(resultSet: ResultSetSummary | ResultSetSummary[]) { + let resultsToUpdate: ResultSetSummary[]; if (!Array.isArray(resultSet)) { resultsToUpdate = [resultSet]; } else { @@ -203,7 +202,7 @@ export class GridPanel extends Disposable { } } - private addResultSet(resultSet: azdata.ResultSetSummary[]) { + private addResultSet(resultSet: ResultSetSummary[]) { let tables: GridTable[] = []; for (let set of resultSet) { @@ -316,7 +315,7 @@ export class GridPanel extends Disposable { export interface IDataSet { rowCount: number; - columnInfo: azdata.IDbColumn[]; + columnInfo: IColumn[]; } export abstract class GridTableBase extends Disposable implements IView { @@ -363,7 +362,7 @@ export abstract class GridTableBase extends Disposable implements IView { constructor( state: GridTableState, - protected _resultSet: azdata.ResultSetSummary, + protected _resultSet: ResultSetSummary, protected contextMenuService: IContextMenuService, protected instantiationService: IInstantiationService, protected editorService: IEditorService, @@ -394,7 +393,7 @@ export abstract class GridTableBase extends Disposable implements IView { abstract get gridDataProvider(): IGridDataProvider; - public get resultSet(): azdata.ResultSetSummary { + public get resultSet(): ResultSetSummary { return this._resultSet; } @@ -587,7 +586,7 @@ export abstract class GridTableBase extends Disposable implements IView { // handle if a showplan link was clicked if (column && (column.isXml || column.isJson)) { this.gridDataProvider.getRowData(event.cell.row, 1).then(async d => { - let value = d.resultSubset.rows[0][event.cell.cell - 1]; + let value = d.rows[0][event.cell.cell - 1]; let content = value.displayValue; const input = this.untitledEditorService.create({ mode: column.isXml ? 'xml' : 'json', initialValue: content }); @@ -598,7 +597,7 @@ export abstract class GridTableBase extends Disposable implements IView { } } - public updateResult(resultSet: azdata.ResultSetSummary) { + public updateResult(resultSet: ResultSetSummary) { this._resultSet = resultSet; if (this.table && this.visible) { this.dataProvider.length = resultSet.rowCount; @@ -655,10 +654,10 @@ export abstract class GridTableBase extends Disposable implements IView { private loadData(offset: number, count: number): Thenable { return this.gridDataProvider.getRowData(offset, count).then(response => { - if (!response.resultSubset) { + if (!response) { return []; } - return response.resultSubset.rows.map(r => { + return response.rows.map(r => { let dataWithSchema = {}; // skip the first column since its a number column for (let i = 1; i < this.columns.length; i++) { @@ -756,7 +755,7 @@ class GridTable extends GridTableBase { private _gridDataProvider: IGridDataProvider; constructor( private _runner: QueryRunner, - resultSet: azdata.ResultSetSummary, + resultSet: ResultSetSummary, state: GridTableState, @IContextMenuService contextMenuService: IContextMenuService, @IInstantiationService instantiationService: IInstantiationService, diff --git a/src/sql/workbench/contrib/query/browser/messagePanel.ts b/src/sql/workbench/contrib/query/browser/messagePanel.ts index d5fd3bc7c3..edfd31dfb4 100644 --- a/src/sql/workbench/contrib/query/browser/messagePanel.ts +++ b/src/sql/workbench/contrib/query/browser/messagePanel.ts @@ -4,9 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/messagePanel'; -import QueryRunner, { IQueryMessage } from 'sql/workbench/services/query/common/queryRunner'; - -import { ISelectionData } from 'azdata'; +import QueryRunner from 'sql/workbench/services/query/common/queryRunner'; +import { IQueryMessage } from 'sql/workbench/services/query/common/query'; import { ITreeRenderer, IDataSource, ITreeNode, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; import { generateUuid } from 'vs/base/common/uuid'; @@ -32,6 +31,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { QueryEditor } from 'sql/workbench/contrib/query/browser/queryEditor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IDataTreeViewState } from 'vs/base/browser/ui/tree/dataTree'; +import { IRange } from 'vs/editor/common/core/range'; export interface IResultMessageIntern { id?: string; @@ -39,8 +39,7 @@ export interface IResultMessageIntern { isError: boolean; time?: string | Date; message: string; - selection?: ISelectionData; - + range?: IRange; } export interface IMessagePanelMessage { @@ -49,7 +48,7 @@ export interface IMessagePanelMessage { } export interface IMessagePanelBatchMessage extends IMessagePanelMessage { - selection: ISelectionData; + range: IRange; time: string; } @@ -267,7 +266,7 @@ class MessagePanelDelegate extends CachedListVirtualDelegate { let editor = this.editorService.activeEditorPane as QueryEditor; const codeEditor = editor.getControl(); codeEditor.focus(); - codeEditor.setSelection(selection); - codeEditor.revealRangeInCenterIfOutsideViewport(selection); + codeEditor.setSelection(node.element.range); + codeEditor.revealRangeInCenterIfOutsideViewport(node.element.range); })); } } diff --git a/src/sql/workbench/contrib/query/browser/queryActions.ts b/src/sql/workbench/contrib/query/browser/queryActions.ts index c05cf92bbf..faf8e2de18 100644 --- a/src/sql/workbench/contrib/query/browser/queryActions.ts +++ b/src/sql/workbench/contrib/query/browser/queryActions.ts @@ -14,7 +14,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import Severity from 'vs/base/common/severity'; import { append, $ } from 'vs/base/browser/dom'; -import { ISelectionData, QueryExecutionOptions } from 'azdata'; +import { QueryExecutionOptions } from 'azdata'; import { IConnectionManagementService, IConnectionParams, @@ -43,6 +43,7 @@ import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilit import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; import { IQueryManagementService } from 'sql/workbench/services/query/common/queryManagement'; import { ILogService } from 'vs/platform/log/common/log'; +import { IRange } from 'vs/editor/common/core/range'; /** * Action class that query-based Actions will extend. This base class automatically handles activating and @@ -101,12 +102,12 @@ export abstract class QueryTaskbarAction extends Action { * Connects the given editor to it's current URI. * Public for testing only. */ - protected connectEditor(editor: QueryEditor, runQueryOnCompletion?: RunQueryOnConnectionMode, selection?: ISelectionData): void { + protected connectEditor(editor: QueryEditor, runQueryOnCompletion?: RunQueryOnConnectionMode, range?: IRange): void { let params: INewConnectionParams = { input: editor.input, connectionType: ConnectionType.editor, runQueryOnCompletion: runQueryOnCompletion ? runQueryOnCompletion : RunQueryOnConnectionMode.none, - querySelection: selection + queryRange: range }; this.connectionManagementService.showConnectionDialog(params); } @@ -241,8 +242,8 @@ export class RunQueryAction extends QueryTaskbarAction { if (this.isConnected(editor)) { // if the selection isn't empty then execute the selection // otherwise, either run the statement or the script depending on parameter - let selection: ISelectionData = editor.getSelection(false); - if (runCurrentStatement && selection && this.isCursorPosition(selection)) { + let selection = editor.getSelection(); + if (runCurrentStatement && selection) { editor.input.runQueryStatement(selection); } else { // get the selection again this time with trimming @@ -251,11 +252,6 @@ export class RunQueryAction extends QueryTaskbarAction { } } } - - protected isCursorPosition(selection: ISelectionData) { - return selection.startLine === selection.endLine - && selection.startColumn === selection.endColumn; - } } /** diff --git a/src/sql/workbench/contrib/query/browser/queryEditor.ts b/src/sql/workbench/contrib/query/browser/queryEditor.ts index 79ffca210e..5e2177b486 100644 --- a/src/sql/workbench/contrib/query/browser/queryEditor.ts +++ b/src/sql/workbench/contrib/query/browser/queryEditor.ts @@ -23,7 +23,6 @@ import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor import { SplitView, Sizing } from 'vs/base/browser/ui/splitview/splitview'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { ISelectionData } from 'azdata'; import { IActionViewItem, IAction } from 'vs/base/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; @@ -36,6 +35,7 @@ import { QueryResultsEditor } from 'sql/workbench/contrib/query/browser/queryRes import * as queryContext from 'sql/workbench/contrib/query/common/queryContext'; import { Taskbar, ITaskbarContent } from 'sql/base/browser/ui/taskbar/taskbar'; import * as actions from 'sql/workbench/contrib/query/browser/queryActions'; +import { IRange } from 'vs/editor/common/core/range'; const QUERY_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'queryEditorViewState'; @@ -499,7 +499,7 @@ export class QueryEditor extends BaseEditor { * Returns the underlying SQL editor's text selection in a 0-indexed format. Returns undefined if there * is no selected text. */ - public getSelection(checkIfRange: boolean = true): ISelectionData { + public getSelection(checkIfRange: boolean = true): IRange { if (this.currentTextEditor && this.currentTextEditor.getControl()) { let vscodeSelection = this.currentTextEditor.getControl().getSelection(); @@ -508,13 +508,7 @@ export class QueryEditor extends BaseEditor { !(vscodeSelection.getStartPosition().lineNumber === vscodeSelection.getEndPosition().lineNumber && vscodeSelection.getStartPosition().column === vscodeSelection.getEndPosition().column); if (!checkIfRange || isRange) { - let sqlToolsServiceSelection: ISelectionData = { - startLine: vscodeSelection.getStartPosition().lineNumber - 1, - startColumn: vscodeSelection.getStartPosition().column - 1, - endLine: vscodeSelection.getEndPosition().lineNumber - 1, - endColumn: vscodeSelection.getEndPosition().column - 1, - }; - return sqlToolsServiceSelection; + return vscodeSelection; } } @@ -522,7 +516,7 @@ export class QueryEditor extends BaseEditor { return undefined; } - public getAllSelection(): ISelectionData { + public getAllSelection(): IRange { if (this.currentTextEditor && this.currentTextEditor.getControl()) { let control = this.currentTextEditor.getControl(); let codeEditor: ICodeEditor = control; @@ -530,13 +524,12 @@ export class QueryEditor extends BaseEditor { let model = codeEditor.getModel(); let totalLines = model.getLineCount(); let endColumn = model.getLineMaxColumn(totalLines); - let selection: ISelectionData = { - startLine: 0, - startColumn: 0, - endLine: totalLines - 1, - endColumn: endColumn - 1, + return { + startLineNumber: 1, + startColumn: 1, + endLineNumber: totalLines, + endColumn: endColumn, }; - return selection; } } return undefined; diff --git a/src/sql/workbench/contrib/query/test/browser/queryActions.test.ts b/src/sql/workbench/contrib/query/test/browser/queryActions.test.ts index 64e199a4c5..a42fb2c2c2 100644 --- a/src/sql/workbench/contrib/query/test/browser/queryActions.test.ts +++ b/src/sql/workbench/contrib/query/test/browser/queryActions.test.ts @@ -5,8 +5,6 @@ import { Emitter, Event } from 'vs/base/common/event'; -import { ISelectionData } from 'azdata'; - import { IConnectionParams, INewConnectionParams, @@ -34,6 +32,7 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; +import { IRange } from 'vs/editor/common/core/range'; suite('SQL QueryAction Tests', () => { @@ -222,9 +221,9 @@ suite('SQL QueryAction Tests', () => { let countCalledShowDialog: number = 0; let countCalledRunQuery: number = 0; let showDialogConnectionParams: INewConnectionParams = undefined; - let runQuerySelection: ISelectionData = undefined; - let selectionToReturnInGetSelection: ISelectionData = undefined; - let predefinedSelection: ISelectionData = { startLine: 1, startColumn: 2, endLine: 3, endColumn: 4 }; + let runQuerySelection: IRange = undefined; + let selectionToReturnInGetSelection: IRange = undefined; + let predefinedSelection: IRange = { startLineNumber: 1, startColumn: 2, endLineNumber: 3, endColumn: 4 }; // ... Mock "getSelection" in QueryEditor const workbenchinstantiationService = workbenchInstantiationService(); @@ -234,11 +233,11 @@ suite('SQL QueryAction Tests', () => { let queryInput = TypeMoq.Mock.ofType(UntitledQueryEditorInput, TypeMoq.MockBehavior.Loose, undefined, fileInput, undefined, connectionManagementService.object, queryModelService.object, configurationService.object); queryInput.setup(x => x.uri).returns(() => testUri); - queryInput.setup(x => x.runQuery(TypeMoq.It.isAny())).callback((selection: ISelectionData) => { + queryInput.setup(x => x.runQuery(TypeMoq.It.isAny())).callback((selection: IRange) => { runQuerySelection = selection; countCalledRunQuery++; }); - queryInput.setup(x => x.runQuery(undefined)).callback((selection: ISelectionData) => { + queryInput.setup(x => x.runQuery(undefined)).callback((selection: IRange) => { runQuerySelection = selection; countCalledRunQuery++; }); @@ -277,7 +276,7 @@ suite('SQL QueryAction Tests', () => { assert.equal(countCalledShowDialog, 1, 'run should call showDialog'); assert.equal(countCalledRunQuery, 0, 'run should not call runQuery'); assert.equal(showDialogConnectionParams.connectionType, ConnectionType.editor, 'connectionType should be queryEditor'); - assert.equal(showDialogConnectionParams.querySelection, undefined, 'querySelection should be undefined'); + assert.equal(showDialogConnectionParams.queryRange, undefined, 'querySelection should be undefined'); ////// If I call run on RunQueryAction while disconnected and with a defined selection isConnected = false; @@ -288,11 +287,11 @@ suite('SQL QueryAction Tests', () => { assert.equal(countCalledShowDialog, 2, 'run should call showDialog again'); assert.equal(countCalledRunQuery, 0, 'run should not call runQuery'); assert.equal(showDialogConnectionParams.connectionType, ConnectionType.editor, 'connectionType should be queryEditor'); - assert.notEqual(showDialogConnectionParams.querySelection, undefined, 'There should not be an undefined selection in runQuery'); - assert.equal(showDialogConnectionParams.querySelection.startLine, selectionToReturnInGetSelection.startLine, 'startLine should match'); - assert.equal(showDialogConnectionParams.querySelection.startColumn, selectionToReturnInGetSelection.startColumn, 'startColumn should match'); - assert.equal(showDialogConnectionParams.querySelection.endLine, selectionToReturnInGetSelection.endLine, 'endLine should match'); - assert.equal(showDialogConnectionParams.querySelection.endColumn, selectionToReturnInGetSelection.endColumn, 'endColumn should match'); + assert.notEqual(showDialogConnectionParams.queryRange, undefined, 'There should not be an undefined selection in runQuery'); + assert.equal(showDialogConnectionParams.queryRange.startLineNumber, selectionToReturnInGetSelection.startLineNumber, 'startLine should match'); + assert.equal(showDialogConnectionParams.queryRange.startColumn, selectionToReturnInGetSelection.startColumn, 'startColumn should match'); + assert.equal(showDialogConnectionParams.queryRange.endLineNumber, selectionToReturnInGetSelection.endLineNumber, 'endLine should match'); + assert.equal(showDialogConnectionParams.queryRange.endColumn, selectionToReturnInGetSelection.endColumn, 'endColumn should match'); ////// If I call run on RunQueryAction while connected and with an undefined selection isConnected = true; @@ -313,9 +312,9 @@ suite('SQL QueryAction Tests', () => { assert.equal(countCalledShowDialog, 2, 'run should not call showDialog'); assert.equal(countCalledRunQuery, 2, 'run should call runQuery again'); assert.notEqual(runQuerySelection, undefined, 'There should not be an undefined selection in runQuery'); - assert.equal(runQuerySelection.startLine, selectionToReturnInGetSelection.startLine, 'startLine should match'); + assert.equal(runQuerySelection.startLineNumber, selectionToReturnInGetSelection.startLineNumber, 'startLine should match'); assert.equal(runQuerySelection.startColumn, selectionToReturnInGetSelection.startColumn, 'startColumn should match'); - assert.equal(runQuerySelection.endLine, selectionToReturnInGetSelection.endLine, 'endLine should match'); + assert.equal(runQuerySelection.endLineNumber, selectionToReturnInGetSelection.endLineNumber, 'endLine should match'); assert.equal(runQuerySelection.endColumn, selectionToReturnInGetSelection.endColumn, 'endColumn should match'); }); diff --git a/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts b/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts index b31a55ba51..06e646e759 100644 --- a/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts +++ b/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts @@ -349,7 +349,7 @@ suite('SQL ConnectionManagementService tests', () => { onConnectCanceled: undefined, uri: uri }, - querySelection: undefined, + queryRange: undefined, runQueryOnCompletion: RunQueryOnConnectionMode.none }, saveTheConnection: true, @@ -456,7 +456,7 @@ suite('SQL ConnectionManagementService tests', () => { onConnectCanceled: undefined, uri: uri1, }, - querySelection: undefined, + queryRange: undefined, runQueryOnCompletion: RunQueryOnConnectionMode.none, isEditConnection: false }, @@ -494,7 +494,7 @@ suite('SQL ConnectionManagementService tests', () => { onConnectCanceled: undefined, uri: uri1 }, - querySelection: undefined, + queryRange: undefined, runQueryOnCompletion: RunQueryOnConnectionMode.none, isEditConnection: false }, @@ -705,7 +705,7 @@ suite('SQL ConnectionManagementService tests', () => { onConnectCanceled: undefined, uri: uri }, - querySelection: undefined, + queryRange: undefined, runQueryOnCompletion: RunQueryOnConnectionMode.none }, saveTheConnection: true, diff --git a/src/sql/workbench/services/editData/common/editQueryRunner.ts b/src/sql/workbench/services/editData/common/editQueryRunner.ts new file mode 100644 index 0000000000..805465275b --- /dev/null +++ b/src/sql/workbench/services/editData/common/editQueryRunner.ts @@ -0,0 +1,123 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as nls from 'vs/nls'; + +import QueryRunner from 'sql/workbench/services/query/common/queryRunner'; +import Severity from 'vs/base/common/severity'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IQueryManagementService } from 'sql/workbench/services/query/common/queryManagement'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { Emitter } from 'vs/base/common/event'; + +export interface IEditSessionReadyEvent { + ownerUri: string; + success: boolean; + message: string; +} + +export default class EditQueryRunner extends QueryRunner { + + private readonly _onEditSessionReady = this._register(new Emitter()); + public readonly onEditSessionReady = this._onEditSessionReady.event; + + constructor( + public uri: string, + @INotificationService private readonly notificationService: INotificationService, + @IQueryManagementService queryManagementService: IQueryManagementService, + @IConfigurationService configurationService: IConfigurationService, + @IInstantiationService instantiationService: IInstantiationService, + @ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService, + @ILogService logService: ILogService + ) { + super(uri, queryManagementService, configurationService, instantiationService, textResourcePropertiesService, logService); + } + + /* + * Handle a session ready event for Edit Data + */ + public async initializeEdit(ownerUri: string, schemaName: string, objectName: string, objectType: string, rowLimit: number, queryString: string): Promise { + // Update internal state to show that we're executing the query + this._isExecuting = true; + this._totalElapsedMilliseconds = 0; + // TODO issue #228 add statusview callbacks here + + try { + await this.queryManagementService.initializeEdit(ownerUri, schemaName, objectName, objectType, rowLimit, queryString); + // The query has started, so lets fire up the result pane + this._onQueryStart.fire(); + this.queryManagementService.registerRunner(this, ownerUri); + } catch (error) { + // Attempting to launch the query failed, show the error message + + // TODO issue #228 add statusview callbacks here + this._isExecuting = false; + this.notificationService.error(nls.localize('query.initEditExecutionFailed', "Initialize edit data session 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 async getEditRows(rowStart: number, numberOfRows: number): Promise { + let rowData: azdata.EditSubsetParams = { + ownerUri: this.uri, + rowCount: numberOfRows, + rowStartIndex: rowStart + }; + + const result = await this.queryManagementService.getEditRows(rowData); + if (!result.hasOwnProperty('rowCount')) { + let error = `Nothing returned from subset query`; + this.notificationService.notify({ + severity: Severity.Error, + message: error + }); + throw new Error(error); + } + return result; + } + + public handleEditSessionReady(ownerUri: string, success: boolean, message: string): void { + this._onEditSessionReady.fire({ ownerUri, success, message }); + } + + public updateCell(ownerUri: string, rowId: number, columnId: number, newValue: string): Promise { + return this.queryManagementService.updateCell(ownerUri, rowId, columnId, newValue); + } + + public commitEdit(ownerUri: string): Promise { + return this.queryManagementService.commitEdit(ownerUri); + } + + public createRow(ownerUri: string): Promise { + return this.queryManagementService.createRow(ownerUri).then(result => { + return result; + }); + } + + public deleteRow(ownerUri: string, rowId: number): Promise { + return this.queryManagementService.deleteRow(ownerUri, rowId); + } + + public revertCell(ownerUri: string, rowId: number, columnId: number): Promise { + return this.queryManagementService.revertCell(ownerUri, rowId, columnId); + } + + public revertRow(ownerUri: string, rowId: number): Promise { + return this.queryManagementService.revertRow(ownerUri, rowId); + } + + public disposeEdit(ownerUri: string): Promise { + return this.queryManagementService.disposeEdit(ownerUri); + } + +} diff --git a/src/sql/workbench/services/insights/browser/insightsDialogController.ts b/src/sql/workbench/services/insights/browser/insightsDialogController.ts index 1c1dddad35..40079bc5ac 100644 --- a/src/sql/workbench/services/insights/browser/insightsDialogController.ts +++ b/src/sql/workbench/services/insights/browser/insightsDialogController.ts @@ -6,12 +6,11 @@ import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import QueryRunner from 'sql/workbench/services/query/common/queryRunner'; +import { IColumn, ICellValue, ResultSetSubset } from 'sql/workbench/services/query/common/query'; import * as Utils from 'sql/platform/connection/common/utils'; import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService'; import { resolveQueryFilePath } from '../common/insightsUtils'; -import { DbCellValue, IDbColumn, QueryExecuteSubsetResult } from 'azdata'; - import Severity from 'vs/base/common/severity'; import * as types from 'vs/base/common/types'; import * as nls from 'vs/nls'; @@ -27,8 +26,8 @@ export class InsightsDialogController { private _queryRunner: QueryRunner; private _connectionProfile: IConnectionProfile; private _connectionUri: string; - private _columns: IDbColumn[]; - private _rows: DbCellValue[][]; + private _columns: IColumn[]; + private _rows: ICellValue[][]; constructor( private readonly _model: IInsightsDialogModel, @@ -160,13 +159,13 @@ export class InsightsDialogController { ) { let resultset = batch.resultSetSummaries[0]; this._columns = resultset.columnInfo; - let rows: QueryExecuteSubsetResult; + let rows: ResultSetSubset; try { rows = await this._queryRunner.getQueryRows(0, resultset.rowCount, batch.id, resultset.id); } catch (e) { return Promise.reject(e); } - this._rows = rows.resultSubset.rows; + this._rows = rows.rows; this.updateModel(); } } diff --git a/src/sql/workbench/services/insights/test/browser/insightsDialogController.test.ts b/src/sql/workbench/services/insights/test/browser/insightsDialogController.test.ts index 4a4e8a5782..2d9741ed73 100644 --- a/src/sql/workbench/services/insights/test/browser/insightsDialogController.test.ts +++ b/src/sql/workbench/services/insights/test/browser/insightsDialogController.test.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { InsightsDialogController } from 'sql/workbench/services/insights/browser/insightsDialogController'; -import QueryRunner, { IQueryMessage } from 'sql/workbench/services/query/common/queryRunner'; +import QueryRunner from 'sql/workbench/services/query/common/queryRunner'; +import { IQueryMessage, BatchSummary, IColumn, ResultSetSubset } from 'sql/workbench/services/query/common/query'; import { ConnectionManagementService } from 'sql/workbench/services/connection/browser/connectionManagementService'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; -import * as azdata from 'azdata'; import { equal } from 'assert'; import { Mock, MockBehavior, It } from 'typemoq'; import { Emitter } from 'vs/base/common/event'; @@ -114,12 +114,12 @@ function getPrimedQueryRunner(data: string[][], columns: string[]): IPrimedQuery querymock.setup(x => x.onQueryEnd).returns(x => emitter.event); querymock.setup(x => x.onMessage).returns(x => new Emitter<[IQueryMessage]>().event); querymock.setup(x => x.batchSets).returns(x => { - return >[ + return >[ { id: 0, resultSetSummaries: [ { - columnInfo: >columns.map(c => { return { columnName: c }; }), + columnInfo: >columns.map(c => { return { columnName: c }; }), id: 0, rowCount: data.length } @@ -129,11 +129,9 @@ function getPrimedQueryRunner(data: string[][], columns: string[]): IPrimedQuery }); querymock.setup(x => x.getQueryRows(It.isAnyNumber(), It.isAnyNumber(), It.isAnyNumber(), It.isAnyNumber())) - .returns(x => Promise.resolve({ - resultSubset: { - rowCount: data.length, - rows: data.map(r => r.map(c => { return { displayValue: c }; })) - } + .returns(x => Promise.resolve({ + rowCount: data.length, + rows: data.map(r => r.map(c => { return { displayValue: c }; })) })); querymock.setup(x => x.runQuery(It.isAnyString())).returns(x => Promise.resolve()); diff --git a/src/sql/workbench/services/notebook/browser/sql/sqlSessionManager.ts b/src/sql/workbench/services/notebook/browser/sql/sqlSessionManager.ts index f653d6f34e..1b8dea0c67 100644 --- a/src/sql/workbench/services/notebook/browser/sql/sqlSessionManager.ts +++ b/src/sql/workbench/services/notebook/browser/sql/sqlSessionManager.ts @@ -3,9 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { nb, QueryExecuteSubsetResult, IDbColumn, BatchSummary, IResultMessage, ResultSetSummary } from 'azdata'; +import { nb, IResultMessage } from 'azdata'; import { localize } from 'vs/nls'; import QueryRunner from 'sql/workbench/services/query/common/queryRunner'; +import { BatchSummary, ResultSetSummary, IColumn, ResultSetSubset } from 'sql/workbench/services/query/common/query'; import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import Severity from 'vs/base/common/severity'; @@ -343,7 +344,7 @@ class SqlKernel extends Disposable implements nb.IKernel { this._register(queryRunner.onMessage(messages => { // TODO handle showing a messages output (should be updated with all messages, only changing 1 output in total) for (const message of messages) { - if (this._future && isUndefinedOrNull(message.selection)) { + if (this._future && isUndefinedOrNull(message.range)) { this._future.handleMessage(message); } } @@ -383,7 +384,7 @@ export class SQLFuture extends Disposable implements FutureInternal { private doneDeferred = new Deferred(); private configuredMaxRows: number = MAX_ROWS; private _outputAddedPromises: Promise[] = []; - private _querySubsetResultMap: Map = new Map(); + private _querySubsetResultMap: Map = new Map(); private _errorOccurred: boolean = false; private _stopOnError: boolean = true; constructor( @@ -508,11 +509,11 @@ export class SQLFuture extends Disposable implements FutureInternal { this._querySubsetResultMap.set(resultSet.id, result); deferred.resolve(); }, (err) => { - this._querySubsetResultMap.set(resultSet.id, { message: '', resultSubset: { rowCount: 0, rows: [] } }); + this._querySubsetResultMap.set(resultSet.id, { rowCount: 0, rows: [] }); deferred.reject(err); }); } else { - this._querySubsetResultMap.set(resultSet.id, { message: '', resultSubset: { rowCount: 0, rows: [] } }); + this._querySubsetResultMap.set(resultSet.id, { rowCount: 0, rows: [] }); deferred.resolve(); } return deferred; @@ -525,7 +526,7 @@ export class SQLFuture extends Disposable implements FutureInternal { } } - private sendIOPubMessage(subsetResult: QueryExecuteSubsetResult, resultSet: ResultSetSummary): void { + private sendIOPubMessage(subsetResult: ResultSetSubset, resultSet: ResultSetSummary): void { let msg: nb.IIOPubMessage = { channel: 'iopub', type: 'iopub', @@ -560,7 +561,7 @@ export class SQLFuture extends Disposable implements FutureInternal { // no-op } - private convertToDataResource(columns: IDbColumn[], subsetResult: QueryExecuteSubsetResult): IDataResource { + private convertToDataResource(columns: IColumn[], subsetResult: ResultSetSubset): IDataResource { let columnsResources: IDataResourceSchema[] = []; columns.forEach(column => { columnsResources.push({ name: escape(column.columnName) }); @@ -569,7 +570,7 @@ export class SQLFuture extends Disposable implements FutureInternal { columnsFields.fields = columnsResources; return { schema: columnsFields, - data: subsetResult.resultSubset.rows.map(row => { + data: subsetResult.rows.map(row => { let rowObject: { [key: string]: any; } = {}; row.forEach((val, index) => { rowObject[index] = val.displayValue; @@ -579,9 +580,9 @@ export class SQLFuture extends Disposable implements FutureInternal { }; } - private convertToHtmlTable(columns: IDbColumn[], d: QueryExecuteSubsetResult): string[] { + private convertToHtmlTable(columns: IColumn[], d: ResultSetSubset): string[] { // Adding 3 for , column title rows,
- let htmlStringArr: string[] = new Array(d.resultSubset.rowCount + 3); + let htmlStringArr: string[] = new Array(d.rowCount + 3); htmlStringArr[0] = ''; if (columns.length > 0) { let columnHeaders = ''; @@ -592,7 +593,7 @@ export class SQLFuture extends Disposable implements FutureInternal { htmlStringArr[1] = columnHeaders; } let i = 2; - for (const row of d.resultSubset.rows) { + for (const row of d.rows) { let rowData = ''; for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) { rowData += ``; diff --git a/src/sql/workbench/services/query/common/gridDataProvider.ts b/src/sql/workbench/services/query/common/gridDataProvider.ts index 0532cd954b..8dda4b49a1 100644 --- a/src/sql/workbench/services/query/common/gridDataProvider.ts +++ b/src/sql/workbench/services/query/common/gridDataProvider.ts @@ -3,9 +3,9 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as azdata from 'azdata'; import * as types from 'vs/base/common/types'; import { SaveFormat } from 'sql/workbench/services/query/common/resultSerializer'; +import { ResultSetSubset } from 'sql/workbench/services/query/common/query'; export interface IGridDataProvider { @@ -14,7 +14,7 @@ export interface IGridDataProvider { * @param rowStart 0-indexed start row to retrieve data from * @param numberOfRows total number of rows of data to retrieve */ - getRowData(rowStart: number, numberOfRows: number): Thenable; + getRowData(rowStart: number, numberOfRows: number): Thenable; /** * Sends a copy request to copy data to the clipboard @@ -65,8 +65,8 @@ export async function getResultsString(provider: IGridDataProvider, selection: S } } // 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]; + for (let rowIndex: number = 0; rowIndex < result.rows.length; rowIndex++) { + let row = result.rows[rowIndex]; let cellObjects = row.slice(range.fromCell, (range.toCell + 1)); // Remove newlines if requested let cells = provider.shouldRemoveNewLines() diff --git a/src/sql/workbench/services/query/common/query.ts b/src/sql/workbench/services/query/common/query.ts new file mode 100644 index 0000000000..e13f2dbfff --- /dev/null +++ b/src/sql/workbench/services/query/common/query.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IRange } from 'vs/editor/common/core/range'; + +export interface IColumn { + columnName: string; + isXml?: boolean; + isJson?: boolean; +} + +export interface ResultSetSummary { + id: number; + batchId: number; + rowCount: number; + columnInfo: IColumn[]; + complete: boolean; +} + +export interface BatchStartSummary { + id: number; + executionStart: string; + range?: IRange; +} + +export interface BatchSummary extends BatchStartSummary { + hasError: boolean; + resultSetSummaries: ResultSetSummary[]; +} + +export interface CompleteBatchSummary extends BatchSummary { + executionElapsed: string; + executionEnd: string; +} + +export interface IQueryMessage { + batchId?: number; + isError: boolean; + time?: string; + message: string; + range?: IRange; +} + +export interface IResultMessage { + batchId?: number; + isError: boolean; + time?: string; + message: string; +} + +export interface QueryExecuteSubsetParams { + ownerUri: string; + batchIndex: number; + resultSetIndex: number; + rowsStartIndex: number; + rowsCount: number; +} + +export interface ResultSetSubset { + rowCount: number; + rows: ICellValue[][]; +} + +export interface ICellValue { + displayValue: string; + isNull?: boolean; +} diff --git a/src/sql/workbench/services/query/common/queryManagement.ts b/src/sql/workbench/services/query/common/queryManagement.ts index f8514829f8..c95953c75f 100644 --- a/src/sql/workbench/services/query/common/queryManagement.ts +++ b/src/sql/workbench/services/query/common/queryManagement.ts @@ -13,11 +13,24 @@ import { Event, Emitter } from 'vs/base/common/event'; import { keys } from 'vs/base/common/map'; import { assign } from 'vs/base/common/objects'; import { IAdsTelemetryService, ITelemetryEventProperties } from 'sql/platform/telemetry/common/telemetry'; +import EditQueryRunner from 'sql/workbench/services/editData/common/editQueryRunner'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { ResultSetSubset } from 'sql/workbench/services/query/common/query'; +import { isUndefined } from 'vs/base/common/types'; export const SERVICE_ID = 'queryManagementService'; export const IQueryManagementService = createDecorator(SERVICE_ID); +export interface QueryCancelResult { + messages: string; +} + +export interface ExecutionPlanOptions { + displayEstimatedQueryPlan?: boolean; + displayActualQueryPlan?: boolean; +} + export interface IQueryManagementService { _serviceBrand: undefined; @@ -28,13 +41,13 @@ export interface IQueryManagementService { getRegisteredProviders(): string[]; registerRunner(runner: QueryRunner, uri: string): void; - cancelQuery(ownerUri: string): Promise; - runQuery(ownerUri: string, selection: azdata.ISelectionData, runOptions?: azdata.ExecutionPlanOptions): Promise; + cancelQuery(ownerUri: string): Promise; + runQuery(ownerUri: string, range: IRange, runOptions?: ExecutionPlanOptions): Promise; runQueryStatement(ownerUri: string, line: number, column: number): Promise; runQueryString(ownerUri: string, queryString: string): Promise; runQueryAndReturn(ownerUri: string, queryString: string): Promise; parseSyntax(ownerUri: string, query: string): Promise; - getQueryRows(rowData: azdata.QueryExecuteSubsetParams): Promise; + getQueryRows(rowData: azdata.QueryExecuteSubsetParams): Promise; disposeQuery(ownerUri: string): Promise; saveResults(requestParams: azdata.SaveResultsRequestParams): Promise; setQueryExecutionOptions(uri: string, options: azdata.QueryExecutionOptions): Promise; @@ -67,7 +80,7 @@ export interface IQueryManagementService { */ export interface IQueryRequestHandler { cancelQuery(ownerUri: string): Promise; - runQuery(ownerUri: string, selection: azdata.ISelectionData, runOptions?: azdata.ExecutionPlanOptions): Promise; + runQuery(ownerUri: string, selection: azdata.ISelectionData, runOptions?: ExecutionPlanOptions): Promise; runQueryStatement(ownerUri: string, line: number, column: number): Promise; runQueryString(ownerUri: string, queryString: string): Promise; runQueryAndReturn(ownerUri: string, queryString: string): Promise; @@ -129,7 +142,7 @@ export class QueryManagementService implements IQueryManagementService { // 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 { + private enqueueOrRun(handlerCallback: (runnerParam: QueryRunner) => void, runner?: QueryRunner): void { if (runner === undefined) { this._handlerCallbackQueue.push(handlerCallback); } else { @@ -137,9 +150,9 @@ export class QueryManagementService implements IQueryManagementService { } } - private _notify(ownerUri: string, sendNotification: (runner: QueryRunner) => void): void { + private _notify(ownerUri: string, sendNotification: (runner: QueryRunner | EditQueryRunner) => void): void { let runner = this._queryRunners.get(ownerUri); - this.enqueueOrRun(sendNotification, runner!); + this.enqueueOrRun(sendNotification, runner); } public addQueryRequestHandler(queryType: string, handler: IQueryRequestHandler): IDisposable { @@ -165,7 +178,7 @@ export class QueryManagementService implements IQueryManagementService { return Array.from(keys(this._requestHandlers)); } - private addTelemetry(eventName: string, ownerUri: string, runOptions?: azdata.ExecutionPlanOptions): void { + private addTelemetry(eventName: string, ownerUri: string, runOptions?: ExecutionPlanOptions): void { const providerId: string = this._connectionService.getProviderIdFromUri(ownerUri); const data: ITelemetryEventProperties = { provider: providerId, @@ -197,51 +210,59 @@ export class QueryManagementService implements IQueryManagementService { } } - public cancelQuery(ownerUri: string): Promise { + public cancelQuery(ownerUri: string): Promise { this.addTelemetry(TelemetryKeys.CancelQuery, ownerUri); return this._runAction(ownerUri, (runner) => { return runner.cancelQuery(ownerUri); }); } - public runQuery(ownerUri: string, selection: azdata.ISelectionData, runOptions?: azdata.ExecutionPlanOptions): Promise { + + public runQuery(ownerUri: string, range?: IRange, runOptions?: ExecutionPlanOptions): Promise { this.addTelemetry(TelemetryKeys.RunQuery, ownerUri, runOptions); return this._runAction(ownerUri, (runner) => { - return runner.runQuery(ownerUri, selection, runOptions); + return runner.runQuery(ownerUri, rangeToSelectionData(range), runOptions); }); } + public runQueryStatement(ownerUri: string, line: number, column: number): Promise { this.addTelemetry(TelemetryKeys.RunQueryStatement, ownerUri); return this._runAction(ownerUri, (runner) => { - return runner.runQueryStatement(ownerUri, line, column); + return runner.runQueryStatement(ownerUri, line - 1, column - 1); // we are taking in a vscode IRange which is 1 indexed, but our api expected a 0 index }); } + public runQueryString(ownerUri: string, queryString: string): Promise { this.addTelemetry(TelemetryKeys.RunQueryString, ownerUri); return this._runAction(ownerUri, (runner) => { return runner.runQueryString(ownerUri, queryString); }); } + public runQueryAndReturn(ownerUri: string, queryString: string): Promise { return this._runAction(ownerUri, (runner) => { return runner.runQueryAndReturn(ownerUri, queryString); }); } + public parseSyntax(ownerUri: string, query: string): Promise { return this._runAction(ownerUri, (runner) => { return runner.parseSyntax(ownerUri, query); }); } - public getQueryRows(rowData: azdata.QueryExecuteSubsetParams): Promise { + + public async getQueryRows(rowData: azdata.QueryExecuteSubsetParams): Promise { return this._runAction(rowData.ownerUri, (runner) => { - return runner.getQueryRows(rowData); + return runner.getQueryRows(rowData).then(r => r.resultSubset); }); } + public disposeQuery(ownerUri: string): Promise { this._queryRunners.delete(ownerUri); return this._runAction(ownerUri, (runner) => { return runner.disposeQuery(ownerUri); }); } + public setQueryExecutionOptions(ownerUri: string, options: azdata.QueryExecutionOptions): Promise { return this._runAction(ownerUri, (runner) => { return runner.setQueryExecutionOptions(ownerUri, options); @@ -256,37 +277,38 @@ export class QueryManagementService implements IQueryManagementService { public onQueryComplete(result: azdata.QueryExecuteCompleteNotificationResult): void { this._notify(result.ownerUri, (runner: QueryRunner) => { - runner.handleQueryComplete(result); + runner.handleQueryComplete(result.batchSummaries.map(s => ({ ...s, range: selectionDataToRange(s.selection) }))); }); } + public onBatchStart(batchInfo: azdata.QueryExecuteBatchNotificationParams): void { this._notify(batchInfo.ownerUri, (runner: QueryRunner) => { - runner.handleBatchStart(batchInfo); + runner.handleBatchStart({ ...batchInfo.batchSummary, range: selectionDataToRange(batchInfo.batchSummary.selection) }); }); } public onBatchComplete(batchInfo: azdata.QueryExecuteBatchNotificationParams): void { this._notify(batchInfo.ownerUri, (runner: QueryRunner) => { - runner.handleBatchComplete(batchInfo); + runner.handleBatchComplete({ range: selectionDataToRange(batchInfo.batchSummary.selection), ...batchInfo.batchSummary }); }); } public onResultSetAvailable(resultSetInfo: azdata.QueryExecuteResultSetNotificationParams): void { this._notify(resultSetInfo.ownerUri, (runner: QueryRunner) => { - runner.handleResultSetAvailable(resultSetInfo); + runner.handleResultSetAvailable(resultSetInfo.resultSetSummary); }); } public onResultSetUpdated(resultSetInfo: azdata.QueryExecuteResultSetNotificationParams): void { this._notify(resultSetInfo.ownerUri, (runner: QueryRunner) => { - runner.handleResultSetUpdated(resultSetInfo); + runner.handleResultSetUpdated(resultSetInfo.resultSetSummary); }); } public onMessage(messagesMap: Map): void { for (const [uri, messages] of messagesMap) { this._notify(uri, (runner: QueryRunner) => { - runner.handleMessage(messages); + runner.handleMessage(messages.map(m => m.message)); }); } } @@ -299,8 +321,8 @@ export class QueryManagementService implements IQueryManagementService { } public onEditSessionReady(ownerUri: string, success: boolean, message: string): void { - this._notify(ownerUri, (runner: QueryRunner) => { - runner.handleEditSessionReady(ownerUri, success, message); + this._notify(ownerUri, runner => { + (runner as EditQueryRunner).handleEditSessionReady(ownerUri, success, message); }); } @@ -352,3 +374,11 @@ export class QueryManagementService implements IQueryManagementService { }); } } + +function selectionDataToRange(selection?: azdata.ISelectionData): IRange | undefined { + return isUndefined(selection) ? undefined : new Range(selection.startLine + 1, selection.startColumn + 1, selection.endLine + 1, selection.endColumn + 1); +} + +function rangeToSelectionData(range?: IRange): azdata.ISelectionData | undefined { + return isUndefined(range) ? undefined : { startLine: range.startLineNumber - 1, startColumn: range.startColumn - 1, endLine: range.endLineNumber - 1, endColumn: range.endColumn - 1 }; +} diff --git a/src/sql/workbench/services/query/common/queryModel.ts b/src/sql/workbench/services/query/common/queryModel.ts index 24f4ace9c3..299cce4111 100644 --- a/src/sql/workbench/services/query/common/queryModel.ts +++ b/src/sql/workbench/services/query/common/queryModel.ts @@ -3,13 +3,12 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import QueryRunner, { IQueryMessage } from 'sql/workbench/services/query/common/queryRunner'; +import QueryRunner from 'sql/workbench/services/query/common/queryRunner'; +import { IQueryMessage, ResultSetSubset } from 'sql/workbench/services/query/common/query'; import { DataService } from 'sql/workbench/services/query/common/dataService'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; import { - ISelectionData, - ResultSetSubset, EditUpdateCellResult, EditSessionReadyParams, EditSubsetResult, @@ -19,6 +18,7 @@ import { queryeditor } from 'azdata'; import { QueryInfo } from 'sql/workbench/services/query/common/queryModelService'; +import { IRange } from 'vs/editor/common/core/range'; export const SERVICE_ID = 'queryModelService'; @@ -31,7 +31,7 @@ export interface IQueryPlanInfo { } export interface IQueryInfo { - selection: ISelectionData[]; + range: IRange[]; messages: IQueryMessage[]; } @@ -51,8 +51,8 @@ export interface IQueryModelService { getQueryRunner(uri: string): QueryRunner | undefined; getQueryRows(uri: string, rowStart: number, numberOfRows: number, batchId: number, resultId: number): Promise; - runQuery(uri: string, selection: ISelectionData | undefined, runOptions?: ExecutionPlanOptions): void; - runQueryStatement(uri: string, selection: ISelectionData | undefined): void; + runQuery(uri: string, range: IRange | undefined, runOptions?: ExecutionPlanOptions): void; + runQueryStatement(uri: string, range: IRange | undefined): void; runQueryString(uri: string, selection: string | undefined): void; cancelQuery(input: QueryRunner | string): void; disposeQuery(uri: string): void; diff --git a/src/sql/workbench/services/query/common/queryModelService.ts b/src/sql/workbench/services/query/common/queryModelService.ts index 976bfa548b..74f1fd0764 100644 --- a/src/sql/workbench/services/query/common/queryModelService.ts +++ b/src/sql/workbench/services/query/common/queryModelService.ts @@ -5,6 +5,7 @@ import * as GridContentEvents from 'sql/workbench/services/query/common/gridContentEvents'; import QueryRunner from 'sql/workbench/services/query/common/queryRunner'; +import { ResultSetSubset } from 'sql/workbench/services/query/common/query'; import { DataService } from 'sql/workbench/services/query/common/dataService'; import { IQueryModelService, IQueryEvent } from 'sql/workbench/services/query/common/queryModel'; @@ -17,6 +18,8 @@ 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'; +import EditQueryRunner from 'sql/workbench/services/editData/common/editQueryRunner'; +import { IRange } from 'vs/editor/common/core/range'; const selectionSnippetMaxLen = 100; @@ -29,10 +32,10 @@ export interface QueryEvent { * Holds information about the state of a query runner */ export class QueryInfo { - public queryRunner?: QueryRunner; + public queryRunner?: EditQueryRunner; public dataService?: DataService; public queryEventQueue?: QueryEvent[]; - public selection?: Array; + public range?: Array; public selectionSnippet?: string; // Notes if the angular components have obtained the DataService. If not, all messages sent @@ -42,7 +45,7 @@ export class QueryInfo { constructor() { this.dataServiceReady = false; this.queryEventQueue = []; - this.selection = []; + this.range = []; } } @@ -128,10 +131,10 @@ export class QueryModelService implements IQueryModelService { /** * Get more data rows from the current resultSets from the service layer */ - public getQueryRows(uri: string, rowStart: number, numberOfRows: number, batchId: number, resultId: number): Promise { + public getQueryRows(uri: string, rowStart: number, numberOfRows: number, batchId: number, resultId: number): Promise { if (this._queryInfoMap.has(uri)) { return this._getQueryInfo(uri)!.queryRunner!.getQueryRows(rowStart, numberOfRows, batchId, resultId).then(results => { - return results.resultSubset; + return results; }); } else { return Promise.resolve(undefined); @@ -170,15 +173,15 @@ export class QueryModelService implements IQueryModelService { /** * Run a query for the given URI with the given text selection */ - public async runQuery(uri: string, selection: azdata.ISelectionData, runOptions?: azdata.ExecutionPlanOptions): Promise { - return this.doRunQuery(uri, selection, false, runOptions); + public async runQuery(uri: string, range: IRange, runOptions?: azdata.ExecutionPlanOptions): Promise { + return this.doRunQuery(uri, range, false, runOptions); } /** * Run the current SQL statement for the given URI */ - public async runQueryStatement(uri: string, selection: azdata.ISelectionData): Promise { - return this.doRunQuery(uri, selection, true); + public async runQueryStatement(uri: string, range: IRange): Promise { + return this.doRunQuery(uri, range, true); } /** @@ -191,7 +194,7 @@ export class QueryModelService implements IQueryModelService { /** * Run Query implementation */ - private async doRunQuery(uri: string, selection: azdata.ISelectionData | string, + private async doRunQuery(uri: string, range: IRange | string, runCurrentStatement: boolean, runOptions?: azdata.ExecutionPlanOptions): Promise { // Reuse existing query runner if it exists let queryRunner: QueryRunner | undefined; @@ -208,7 +211,7 @@ export class QueryModelService implements IQueryModelService { // If the query is not in progress, we can reuse the query runner queryRunner = existingRunner!; - info.selection = []; + info.range = []; info.selectionSnippet = undefined; } else { // We do not have a query runner for this editor, so create a new one @@ -217,23 +220,23 @@ export class QueryModelService implements IQueryModelService { queryRunner = info.queryRunner!; } - if (types.isString(selection)) { + if (types.isString(range)) { // Run the query string in this case - if (selection.length < selectionSnippetMaxLen) { - info.selectionSnippet = selection; + if (range.length < selectionSnippetMaxLen) { + info.selectionSnippet = range; } else { - info.selectionSnippet = selection.substring(0, selectionSnippetMaxLen - 3) + '...'; + info.selectionSnippet = range.substring(0, selectionSnippetMaxLen - 3) + '...'; } - return queryRunner.runQuery(selection, runOptions); + return queryRunner.runQuery(range, runOptions); } else if (runCurrentStatement) { - return queryRunner.runQueryStatement(selection); + return queryRunner.runQueryStatement(range); } else { - return queryRunner.runQuery(selection, runOptions); + return queryRunner.runQuery(range, runOptions); } } private initQueryRunner(uri: string): QueryInfo { - let queryRunner = this._instantiationService.createInstance(QueryRunner, uri); + let queryRunner = this._instantiationService.createInstance(EditQueryRunner, uri); let info = new QueryInfo(); queryRunner.onResultSet(e => { this._fireQueryEvent(uri, 'resultSet', e); @@ -241,14 +244,14 @@ export class QueryModelService implements IQueryModelService { queryRunner.onBatchStart(b => { let link = undefined; let messageText = nls.localize('runQueryBatchStartMessage', "Started executing query at "); - if (b.selection) { + if (b.range) { 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(nls.localize('runQueryBatchStartLine', "Line {0}"), b.selection.startLine + 1) + text: strings.format(nls.localize('runQueryBatchStartLine', "Line {0}"), b.range.startLineNumber) }; } } @@ -260,7 +263,7 @@ export class QueryModelService implements IQueryModelService { link: link }; this._fireQueryEvent(uri, 'message', message); - info.selection!.push(this._validateSelection(b.selection)); + info.range!.push(b.range); }); queryRunner.onMessage(m => { this._fireQueryEvent(uri, 'message', m); @@ -274,7 +277,7 @@ export class QueryModelService implements IQueryModelService { uri: uri, queryInfo: { - selection: info.selection!, + range: info.range!, messages: info.queryRunner!.messages } }; @@ -292,7 +295,7 @@ export class QueryModelService implements IQueryModelService { uri: uri, queryInfo: { - selection: info.selection!, + range: info.range!, messages: info.queryRunner!.messages } }; @@ -308,7 +311,7 @@ export class QueryModelService implements IQueryModelService { uri: uri, queryInfo: { - selection: info.selection!, + range: info.range!, messages: info.queryRunner!.messages } }; @@ -324,7 +327,7 @@ export class QueryModelService implements IQueryModelService { uri: planInfo.fileUri, queryInfo: { - selection: info.selection!, + range: info.range!, messages: info.queryRunner!.messages }, params: planInfo @@ -338,7 +341,7 @@ export class QueryModelService implements IQueryModelService { uri: uri, queryInfo: { - selection: info.selection!, + range: info.range!, messages: info.queryRunner!.messages }, params: resultSetInfo @@ -399,12 +402,12 @@ export class QueryModelService implements IQueryModelService { // EDIT DATA METHODS ///////////////////////////////////////////////////// async initializeEdit(ownerUri: string, schemaName: string, objectName: string, objectType: string, rowLimit: number, queryString: string): Promise { // Reuse existing query runner if it exists - let queryRunner: QueryRunner; + let queryRunner: EditQueryRunner; let info: QueryInfo; if (this._queryInfoMap.has(ownerUri)) { info = this._getQueryInfo(ownerUri)!; - let existingRunner: QueryRunner = info.queryRunner!; + let existingRunner = info.queryRunner!; // If the initialization is already in progress if (existingRunner.isExecuting) { @@ -417,7 +420,7 @@ export class QueryModelService implements IQueryModelService { // 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); + queryRunner = this._instantiationService.createInstance(EditQueryRunner, ownerUri); const resultSetEventType = 'resultSet'; queryRunner.onResultSet(resultSet => { this._fireQueryEvent(ownerUri, resultSetEventType, resultSet); @@ -428,14 +431,14 @@ export class QueryModelService implements IQueryModelService { queryRunner.onBatchStart(batch => { let link = undefined; let messageText = nls.localize('runQueryBatchStartMessage', "Started executing query at "); - if (batch.selection) { + if (batch.range) { 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(nls.localize('runQueryBatchStartLine', "Line {0}"), batch.selection.startLine + 1) + text: strings.format(nls.localize('runQueryBatchStartLine', "Line {0}"), batch.range.startLineNumber) }; } } @@ -459,7 +462,7 @@ export class QueryModelService implements IQueryModelService { uri: ownerUri, queryInfo: { - selection: info.selection!, + range: info.range!, messages: info.queryRunner!.messages }, }; @@ -476,7 +479,7 @@ export class QueryModelService implements IQueryModelService { uri: ownerUri, queryInfo: { - selection: info.selection!, + range: info.range!, messages: info.queryRunner!.messages }, }; @@ -596,8 +599,8 @@ export class QueryModelService implements IQueryModelService { // PRIVATE METHODS ////////////////////////////////////////////////////// - private internalGetQueryRunner(ownerUri: string): QueryRunner | undefined { - let queryRunner: QueryRunner | undefined; + private internalGetQueryRunner(ownerUri: string): EditQueryRunner | undefined { + let queryRunner: EditQueryRunner | undefined; if (this._queryInfoMap.has(ownerUri)) { let existingRunner = this._getQueryInfo(ownerUri)!.queryRunner!; // If the query is not already executing then set it up @@ -648,17 +651,4 @@ export class QueryModelService implements IQueryModelService { public _getQueryInfo(uri: string): QueryInfo | undefined { 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: azdata.ISelectionData): azdata.ISelectionData { - if (!selection) { - selection = {}; - } - 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; - } } diff --git a/src/sql/workbench/services/query/common/queryRunner.ts b/src/sql/workbench/services/query/common/queryRunner.ts index 53a54bd844..331c8bd4c9 100644 --- a/src/sql/workbench/services/query/common/queryRunner.ts +++ b/src/sql/workbench/services/query/common/queryRunner.ts @@ -3,15 +3,12 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as azdata from 'azdata'; - -import { IQueryManagementService } from 'sql/workbench/services/query/common/queryManagement'; +import { IQueryManagementService, QueryCancelResult, ExecutionPlanOptions } from 'sql/workbench/services/query/common/queryManagement'; import * as Utils from 'sql/platform/connection/common/utils'; import { Deferred } from 'sql/base/common/promise'; import { IQueryPlanInfo } from 'sql/workbench/services/query/common/queryModel'; import { ResultSerializer, SaveFormat } from 'sql/workbench/services/query/common/resultSerializer'; -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'; @@ -27,20 +24,8 @@ import { IGridDataProvider, getResultsString } from 'sql/workbench/services/quer import { getErrorMessage } from 'vs/base/common/errors'; import { ILogService } from 'vs/platform/log/common/log'; import { find } from 'vs/base/common/arrays'; - -export interface IEditSessionReadyEvent { - ownerUri: string; - success: boolean; - message: string; -} - -export interface IQueryMessage { - batchId?: number; - isError: boolean; - time?: string; - message: string; - selection?: azdata.ISelectionData; -} +import { IRange, Range } from 'vs/editor/common/core/range'; +import { BatchSummary, IQueryMessage, ResultSetSummary, QueryExecuteSubsetParams, CompleteBatchSummary, IResultMessage, ResultSetSubset, BatchStartSummary } from './query'; /* * Query Runner class which handles running a query, reports the results to the content manager, @@ -50,10 +35,10 @@ export default class QueryRunner extends Disposable { // MEMBER VARIABLES //////////////////////////////////////////////////// private _resultLineOffset?: number; private _resultColumnOffset?: number; - private _totalElapsedMilliseconds: number = 0; - private _isExecuting: boolean = false; + protected _totalElapsedMilliseconds: number = 0; + protected _isExecuting: boolean = false; private _hasCompleted: boolean = false; - private _batchSets: azdata.BatchSummary[] = []; + private _batchSets: BatchSummary[] = []; private _messages: IQueryMessage[] = []; private registered = false; @@ -65,31 +50,28 @@ export default class QueryRunner extends Disposable { private _onMessage = this._register(new Emitter()); public get onMessage(): Event { return this._onMessage.event; } // this is the only way typemoq can moq this... needs investigation @todo anthonydresser 5/2/2019 - private _onResultSet = this._register(new Emitter()); + private readonly _onResultSet = this._register(new Emitter()); public readonly onResultSet = this._onResultSet.event; - private _onResultSetUpdate = this._register(new Emitter()); + private readonly _onResultSetUpdate = this._register(new Emitter()); public readonly onResultSetUpdate = this._onResultSetUpdate.event; - private _onQueryStart = this._register(new Emitter()); + protected readonly _onQueryStart = this._register(new Emitter()); public readonly onQueryStart: Event = this._onQueryStart.event; - private _onQueryEnd = this._register(new Emitter()); + private readonly _onQueryEnd = this._register(new Emitter()); public get onQueryEnd(): Event { return this._onQueryEnd.event; } - private _onBatchStart = this._register(new Emitter()); - public readonly onBatchStart: Event = this._onBatchStart.event; + private readonly _onBatchStart = this._register(new Emitter()); + public readonly onBatchStart: Event = this._onBatchStart.event; - private _onBatchEnd = this._register(new Emitter()); - public readonly onBatchEnd: Event = this._onBatchEnd.event; + private readonly _onBatchEnd = this._register(new Emitter()); + public readonly onBatchEnd: Event = this._onBatchEnd.event; - private _onEditSessionReady = this._register(new Emitter()); - public readonly onEditSessionReady = this._onEditSessionReady.event; - - private _onQueryPlanAvailable = this._register(new Emitter()); + private readonly _onQueryPlanAvailable = this._register(new Emitter()); public readonly onQueryPlanAvailable = this._onQueryPlanAvailable.event; - private _onVisualize = this._register(new Emitter()); + private readonly _onVisualize = this._register(new Emitter()); public readonly onVisualize = this._onVisualize.event; private _queryStartTime?: Date; @@ -104,12 +86,11 @@ export default class QueryRunner extends Disposable { // CONSTRUCTOR ///////////////////////////////////////////////////////// constructor( public uri: string, - @IQueryManagementService private _queryManagementService: IQueryManagementService, - @INotificationService private _notificationService: INotificationService, - @IConfigurationService private _configurationService: IConfigurationService, - @IInstantiationService private instantiationService: IInstantiationService, - @ITextResourcePropertiesService private _textResourcePropertiesService: ITextResourcePropertiesService, - @ILogService private _logService: ILogService + @IQueryManagementService protected readonly queryManagementService: IQueryManagementService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService, + @ILogService private readonly logService: ILogService ) { super(); } @@ -125,7 +106,7 @@ export default class QueryRunner extends Disposable { /** * For public use only, for private use, directly access the member */ - public get batchSets(): azdata.BatchSummary[] { + public get batchSets(): BatchSummary[] { return this._batchSets.slice(0); } @@ -141,22 +122,22 @@ export default class QueryRunner extends Disposable { /** * Cancels the running query, if there is one */ - public cancelQuery(): Promise { - return this._queryManagementService.cancelQuery(this.uri); + public cancelQuery(): Promise { + return this.queryManagementService.cancelQuery(this.uri); } /** * Runs the query with the provided query * @param input Query string to execute */ - public runQuery(input: string, runOptions?: azdata.ExecutionPlanOptions): Promise; + public runQuery(input: string, runOptions?: ExecutionPlanOptions): Promise; /** * Runs the query by pulling the query from the document using the provided selection data * @param input selection data */ - public runQuery(input: azdata.ISelectionData, runOptions?: azdata.ExecutionPlanOptions): Promise; - public runQuery(input: string | azdata.ISelectionData, runOptions?: azdata.ExecutionPlanOptions): Promise { - if (types.isString(input)) { + public runQuery(input: IRange | undefined, runOptions?: ExecutionPlanOptions): Promise; + public runQuery(input: string | IRange | undefined, runOptions?: ExecutionPlanOptions): Promise { + if (types.isString(input) || types.isUndefined(input)) { return this.doRunQuery(input, false, runOptions); } else { return this.doRunQuery(input, false, runOptions); @@ -167,7 +148,7 @@ export default class QueryRunner extends Disposable { * Runs the current SQL statement by pulling the query from the document using the provided selection data * @param input selection data */ - public runQueryStatement(input: azdata.ISelectionData): Promise { + public runQueryStatement(input: IRange): Promise { return this.doRunQuery(input, true); } @@ -175,9 +156,9 @@ export default class QueryRunner extends Disposable { * Implementation that runs the query with the provided query * @param input Query string to execute */ - private doRunQuery(input: string, runCurrentStatement: boolean, runOptions?: azdata.ExecutionPlanOptions): Promise; - private doRunQuery(input: azdata.ISelectionData, runCurrentStatement: boolean, runOptions?: azdata.ExecutionPlanOptions): Promise; - private doRunQuery(input: string | azdata.ISelectionData, runCurrentStatement: boolean, runOptions?: azdata.ExecutionPlanOptions): Promise { + private doRunQuery(input: string, runCurrentStatement: boolean, runOptions?: ExecutionPlanOptions): Promise; + private doRunQuery(input: IRange | undefined, runCurrentStatement: boolean, runOptions?: ExecutionPlanOptions): Promise; + private doRunQuery(input: string | IRange | undefined, runCurrentStatement: boolean, runOptions?: ExecutionPlanOptions): Promise { if (this.isExecuting) { return Promise.resolve(); } @@ -187,9 +168,9 @@ export default class QueryRunner extends Disposable { this._queryStartTime = undefined; this._queryEndTime = undefined; this._messages = []; - if (isSelectionOrUndefined(input)) { + if (isRangeOrUndefined(input)) { // Update internal state to show that we're executing the query - this._resultLineOffset = input ? input.startLine : 0; + this._resultLineOffset = input ? input.startLineNumber : 0; this._resultColumnOffset = input ? input.startColumn : 0; this._isExecuting = true; this._totalElapsedMilliseconds = 0; @@ -199,8 +180,8 @@ export default class QueryRunner extends Disposable { // 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)); + ? this.queryManagementService.runQueryStatement(this.uri, input.startLineNumber, input.startColumn).then(() => this.handleSuccessRunQueryResult(), e => this.handleFailureRunQueryResult(e)) + : this.queryManagementService.runQuery(this.uri, input, runOptions).then(() => this.handleSuccessRunQueryResult(), e => this.handleFailureRunQueryResult(e)); } else { // Update internal state to show that we're executing the query this._isExecuting = true; @@ -208,7 +189,7 @@ export default class QueryRunner extends Disposable { this._onQueryStart.fire(); - return this._queryManagementService.runQueryString(this.uri, input).then(() => this.handleSuccessRunQueryResult(), e => this.handleFailureRunQueryResult(e)); + return this.queryManagementService.runQueryString(this.uri, input).then(() => this.handleSuccessRunQueryResult(), e => this.handleFailureRunQueryResult(e)); } } @@ -218,45 +199,39 @@ export default class QueryRunner extends Disposable { // The query has started, so lets fire up the result pane if (!this.registered) { this.registered = true; - this._queryManagementService.registerRunner(this, this.uri); + this.queryManagementService.registerRunner(this, this.uri); } } private handleFailureRunQueryResult(error: any) { // Attempting to launch the query failed, show the error message - const eol = getEolString(this._textResourcePropertiesService, this.uri); + const eol = getEolString(this.textResourcePropertiesService, this.uri); if (error instanceof Error) { error = error.message; } let message = nls.localize('query.ExecutionFailedError', "Execution failed due to an unexpected error: {0}\t{1}", eol, error); - this.handleMessage([{ - ownerUri: this.uri, - message: { - isError: true, - message: message - } + this.handleMessage([{ + isError: true, + message: message }]); - this.handleQueryComplete({ ownerUri: this.uri }); + this.handleQueryComplete(); } /** * Handle a QueryComplete from the service layer */ - public handleQueryComplete(result: azdata.QueryExecuteCompleteNotificationResult): void { + public handleQueryComplete(batchSummaries?: CompleteBatchSummary[]): 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 = batchSummaries ? batchSummaries : []; this._batchSets.map(batch => { - if (batch.selection) { - batch.selection.startLine += this._resultLineOffset!; - batch.selection.startColumn += this._resultColumnOffset!; - batch.selection.endLine += this._resultLineOffset!; - batch.selection.endColumn += this._resultColumnOffset!; + if (batch.range) { + batch.range = new Range(batch.range.startLineNumber + this._resultLineOffset, batch.range.startColumn + this._resultColumnOffset, batch.range.endLineNumber + this._resultLineOffset, batch.range.endColumn + this._resultColumnOffset); } }); @@ -277,28 +252,20 @@ export default class QueryRunner extends Disposable { /** * Handle a BatchStart from the service layer */ - public handleBatchStart(result: azdata.QueryExecuteBatchNotificationParams): void { - let batch = result.batchSummary; - + public handleBatchStart(batch: BatchStartSummary): void { // Recalculate the start and end lines, relative to the result line offset - if (batch.selection) { - batch.selection.startLine += this._resultLineOffset!; - batch.selection.startColumn += this._resultColumnOffset!; - batch.selection.endLine += this._resultLineOffset!; - batch.selection.endColumn += this._resultColumnOffset!; + if (batch.range) { + batch.range = new Range(batch.range.startLineNumber + this._resultLineOffset, batch.range.startColumn + this._resultColumnOffset, batch.range.endLineNumber + this._resultLineOffset, batch.range.endColumn + this._resultColumnOffset); } - // 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; + this._batchSets[batch.id] = { ...batch, resultSetSummaries: [], hasError: false }; - let message = { + let message: IQueryMessage = { // account for index by 1 - message: nls.localize('query.message.startQuery', "Started executing query at Line {0}", batch.selection.startLine + 1), + message: batch.range ? nls.localize('query.message.startQueryWithRange', "Started executing query at Line {0}", batch.range.startLineNumber) : nls.localize('query.message.startQuery', "Started executing batch {0}", batch.id), time: batch.executionStart, - selection: batch.selection, + range: batch.range, isError: false }; this._messages.push(message); @@ -309,9 +276,7 @@ export default class QueryRunner extends Disposable { /** * Handle a BatchComplete from the service layer */ - public handleBatchComplete(result: azdata.QueryExecuteBatchNotificationParams): void { - let batch: azdata.BatchSummary = result.batchSummary; - + public handleBatchComplete(batch: CompleteBatchSummary): void { // Store the batch again to get the rest of the data this._batchSets[batch.id] = batch; let executionTime = (Utils.parseTimeString(batch.executionElapsed) || 0); @@ -327,19 +292,18 @@ export default class QueryRunner extends Disposable { /** * Handle a ResultSetComplete from the service layer */ - public handleResultSetAvailable(result: azdata.QueryExecuteResultSetNotificationParams): void { - if (result && result.resultSetSummary) { - let resultSet = result.resultSetSummary; - let batchSet: azdata.BatchSummary; + public handleResultSetAvailable(resultSet?: ResultSetSummary): void { + if (resultSet) { + let batchSet: BatchSummary; if (!resultSet.batchId) { // Missing the batchId or processing batchId==0. 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 = { + batchSet = { id: 0, - selection: undefined, + range: undefined, hasError: false, resultSetSummaries: [] }; @@ -350,15 +314,15 @@ export default class QueryRunner extends Disposable { } // handle getting queryPlanxml if we need too // check if this result has show plan, this needs work, it won't work for any other provider - let hasShowPlan = !!find(result.resultSetSummary.columnInfo, e => e.columnName === 'Microsoft SQL Server 2005 XML Showplan'); - if (hasShowPlan) { + let hasShowPlan = !!find(resultSet.columnInfo, e => e.columnName === 'Microsoft SQL Server 2005 XML Showplan'); + if (hasShowPlan && resultSet.rowCount > 0) { this._isQueryPlan = true; - this.getQueryRows(0, 1, result.resultSetSummary.batchId, result.resultSetSummary.id).then(e => { - if (e.resultSubset.rows) { - this._planXml.resolve(e.resultSubset.rows[0][0].displayValue); + this.getQueryRows(0, 1, resultSet.batchId, resultSet.id).then(e => { + if (e.rows) { + this._planXml.resolve(e.rows[0][0].displayValue); } - }).catch((e) => this._logService.error(e)); + }).catch((e) => this.logService.error(e)); } // we will just ignore the set if we already have it // ideally this should never happen @@ -370,31 +334,30 @@ export default class QueryRunner extends Disposable { } } - public handleResultSetUpdated(result: azdata.QueryExecuteResultSetNotificationParams): void { - if (result && result.resultSetSummary) { - let resultSet = result.resultSetSummary; - let batchSet: azdata.BatchSummary; + public handleResultSetUpdated(resultSet?: ResultSetSummary): void { + if (resultSet) { + let batchSet: BatchSummary; batchSet = this._batchSets[resultSet.batchId]; // handle getting queryPlanxml if we need too // check if this result has show plan, this needs work, it won't work for any other provider - let hasShowPlan = !!find(result.resultSetSummary.columnInfo, e => e.columnName === 'Microsoft SQL Server 2005 XML Showplan'); + let hasShowPlan = !!resultSet.columnInfo.find(e => e.columnName === 'Microsoft SQL Server 2005 XML Showplan'); if (hasShowPlan) { this._isQueryPlan = true; - this.getQueryRows(0, 1, result.resultSetSummary.batchId, result.resultSetSummary.id).then(e => { + this.getQueryRows(0, 1, resultSet.batchId, resultSet.id).then(e => { - if (e.resultSubset.rows) { - let planXmlString = e.resultSubset.rows[0][0].displayValue; - this._planXml.resolve(e.resultSubset.rows[0][0].displayValue); + if (e.rows) { + let planXmlString = e.rows[0][0].displayValue; + this._planXml.resolve(e.rows[0][0].displayValue); // fire query plan available event if execution is completed - if (result.resultSetSummary.complete) { + if (resultSet.complete) { this._onQueryPlanAvailable.fire({ providerId: mssqlProviderName, - fileUri: result.ownerUri, + fileUri: this.uri, planXml: planXmlString }); } } - }).catch((e) => this._logService.error(e)); + }).catch((e) => this.logService.error(e)); } if (batchSet) { // Store the result set in the batch and emit that a result set has completed @@ -407,8 +370,7 @@ export default class QueryRunner extends Disposable { /** * Handle a Mssage from the service layer */ - public handleMessage(messagesObj: azdata.QueryExecuteMessageParams[]): void { - const messages = messagesObj.map(m => m.message); + public handleMessage(messages: IResultMessage[]): void { this._messages.push(...messages); // Send the message to the results pane @@ -418,8 +380,8 @@ export default class QueryRunner extends Disposable { /** * Get more data rows from the current resultSets from the service layer */ - public getQueryRows(rowStart: number, numberOfRows: number, batchIndex: number, resultSetIndex: number): Promise { - let rowData: azdata.QueryExecuteSubsetParams = { + public getQueryRows(rowStart: number, numberOfRows: number, batchIndex: number, resultSetIndex: number): Promise { + let rowData: QueryExecuteSubsetParams = { ownerUri: this.uri, resultSetIndex: resultSetIndex, rowsCount: numberOfRows, @@ -427,7 +389,7 @@ export default class QueryRunner extends Disposable { batchIndex: batchIndex }; - return this._queryManagementService.getQueryRows(rowData).then(r => r, error => { + 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) @@ -436,104 +398,11 @@ export default class QueryRunner extends Disposable { }); } - /* - * Handle a session ready event for Edit Data - */ - public initializeEdit(ownerUri: string, schemaName: string, objectName: string, objectType: string, rowLimit: number, queryString: string): Promise { - // 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._onQueryStart.fire(); - 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.error(nls.localize('query.initEditExecutionFailed', "Initialize edit data session 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): Promise { - const self = this; - let rowData: azdata.EditSubsetParams = { - ownerUri: this.uri, - rowCount: numberOfRows, - rowStartIndex: rowStart - }; - - return new Promise((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._onEditSessionReady.fire({ ownerUri, success, message }); - } - - public updateCell(ownerUri: string, rowId: number, columnId: number, newValue: string): Promise { - return this._queryManagementService.updateCell(ownerUri, rowId, columnId, newValue); - } - - public commitEdit(ownerUri: string): Promise { - return this._queryManagementService.commitEdit(ownerUri); - } - - public createRow(ownerUri: string): Promise { - return this._queryManagementService.createRow(ownerUri).then(result => { - return result; - }); - } - - public deleteRow(ownerUri: string, rowId: number): Promise { - return this._queryManagementService.deleteRow(ownerUri, rowId); - } - - public revertCell(ownerUri: string, rowId: number, columnId: number): Promise { - return this._queryManagementService.revertCell(ownerUri, rowId, columnId).then(result => { - return result; - }); - } - - public revertRow(ownerUri: string, rowId: number): Promise { - return this._queryManagementService.revertRow(ownerUri, rowId); - } - - public disposeEdit(ownerUri: string): Promise { - return this._queryManagementService.disposeEdit(ownerUri); - } - /** * Disposes the Query from the service client */ public async disposeQuery(): Promise { - await this._queryManagementService.disposeQuery(this.uri); + await this.queryManagementService.disposeQuery(this.uri); this.dispose(); } @@ -561,7 +430,7 @@ export default class QueryRunner extends Disposable { public getColumnHeaders(batchId: number, resultId: number, range: Slick.Range): string[] | undefined { let headers: string[] | undefined = undefined; - let batchSummary: azdata.BatchSummary = this._batchSets[batchId]; + let batchSummary: BatchSummary = this._batchSets[batchId]; if (batchSummary !== undefined) { let resultSetSummary = batchSummary.resultSetSummaries[resultId]; headers = resultSetSummary.columnInfo.slice(range.fromCell, range.toCell + 1).map((info, i) => { @@ -573,7 +442,7 @@ export default class QueryRunner extends Disposable { private sendBatchTimeMessage(batchId: number, executionTime: string): void { // get config copyRemoveNewLine option from vscode config - let showBatchTime = this._configurationService.getValue('sql.showBatchTime'); + let showBatchTime = this.configurationService.getValue('sql.showBatchTime'); if (showBatchTime) { let message: IQueryMessage = { batchId: batchId, @@ -596,7 +465,7 @@ export default class QueryRunner extends Disposable { } public notifyVisualizeRequested(batchId: number, resultSetId: number): void { - let result: azdata.ResultSetSummary = { + let result: ResultSetSummary = { batchId: batchId, id: resultSetId, columnInfo: this.batchSets[batchId].resultSetSummaries[resultSetId].columnInfo, @@ -620,7 +489,7 @@ export class QueryGridDataProvider implements IGridDataProvider { ) { } - getRowData(rowStart: number, numberOfRows: number): Promise { + getRowData(rowStart: number, numberOfRows: number): Promise { return this.queryRunner.getQueryRows(rowStart, numberOfRows, this.batchId, this.resultSetId); } @@ -679,6 +548,6 @@ export function shouldRemoveNewLines(configurationService: IConfigurationService return !!removeNewLines; } -function isSelectionOrUndefined(input: string | azdata.ISelectionData | undefined): input is azdata.ISelectionData | undefined { - return types.isObject(input) || types.isUndefinedOrNull(input); +function isRangeOrUndefined(input: string | IRange | undefined): input is IRange | undefined { + return Range.isIRange(input) || types.isUndefinedOrNull(input); } diff --git a/src/sql/workbench/services/query/test/common/queryRunner.test.ts b/src/sql/workbench/services/query/test/common/queryRunner.test.ts new file mode 100644 index 0000000000..ec4063623d --- /dev/null +++ b/src/sql/workbench/services/query/test/common/queryRunner.test.ts @@ -0,0 +1,229 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import QueryRunner from 'sql/workbench/services/query/common/queryRunner'; +import { BatchSummary, ResultSetSummary, IResultMessage, ResultSetSubset, CompleteBatchSummary } from 'sql/workbench/services/query/common/query'; +import { URI } from 'vs/base/common/uri'; +import { workbenchInstantiationService } from 'sql/workbench/test/workbenchTestServices'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IQueryManagementService } from 'sql/workbench/services/query/common/queryManagement'; +import { Event } from 'vs/base/common/event'; +import { range } from 'vs/base/common/arrays'; + +suite('Query Runner', () => { + test('does execute a standard selection query workflow', async () => { + const instantiationService = workbenchInstantiationService(); + const uri = URI.parse('test:uri').toString(); + const runner = instantiationService.createInstance(QueryRunner, uri); + const runQueryStub = sinon.stub().returns(Promise.resolve()); + (instantiationService as TestInstantiationService).stub(IQueryManagementService, 'runQuery', runQueryStub); + assert(!runner.isExecuting); + assert(!runner.hasCompleted); + // start query + const queryStartPromise = Event.toPromise(runner.onQueryStart); + const rangeSelection = { endColumn: 1, endLineNumber: 1, startColumn: 1, startLineNumber: 1 }; + await runner.runQuery(rangeSelection); + assert(runQueryStub.calledOnce); + assert(runQueryStub.calledWithExactly(uri, rangeSelection, undefined)); + await queryStartPromise; + assert(runner.queryStartTime instanceof Date); + assert(runner.isExecuting); + assert(!runner.hasCompleted); + // start batch + const batch: BatchSummary = { id: 0, hasError: false, range: rangeSelection, resultSetSummaries: [], executionStart: '' }; + const returnBatch = await trigger(batch, arg => runner.handleBatchStart(arg), runner.onBatchStart); + assert.deepEqual(returnBatch, batch); + // we expect the query runner to create a message sense we sent a selection + assert(runner.messages.length === 1); + // start result set + const result1: ResultSetSummary = { batchId: 0, id: 0, complete: false, rowCount: 0, columnInfo: [{ columnName: 'column' }] }; + const returnResult = await trigger(result1, arg => runner.handleResultSetAvailable(arg), runner.onResultSet); + assert.deepEqual(returnResult, result1); + assert.deepEqual(runner.batchSets[0].resultSetSummaries[0], result1); + // update result set + const result1Update: ResultSetSummary = { batchId: 0, id: 0, complete: true, rowCount: 100, columnInfo: [{ columnName: 'column' }] }; + const returnResultUpdate = await trigger(result1Update, arg => runner.handleResultSetUpdated(arg), runner.onResultSetUpdate); + assert.deepEqual(returnResultUpdate, result1Update); + assert.deepEqual(runner.batchSets[0].resultSetSummaries[0], result1Update); + // post message + const message: IResultMessage = { message: 'some message', isError: false, batchId: 0 }; + const messageReturn = await trigger([message], arg => runner.handleMessage(arg), runner.onMessage); + assert.deepEqual(messageReturn[0], message); + assert.deepEqual(runner.messages[1], message); + // get query rows + const rowResults: ResultSetSubset = { rowCount: 100, rows: range(100).map(r => range(1).map(c => ({ displayValue: `${r}${c}` }))) }; + const getRowStub = sinon.stub().returns(Promise.resolve(rowResults)); + (instantiationService as TestInstantiationService).stub(IQueryManagementService, 'getQueryRows', getRowStub); + const resultReturn = await runner.getQueryRows(0, 100, 0, 0); + assert(getRowStub.calledWithExactly({ ownerUri: uri, batchIndex: 0, resultSetIndex: 0, rowsStartIndex: 0, rowsCount: 100 })); + assert.deepStrictEqual(resultReturn, rowResults); + // batch complete + const batchComplete: CompleteBatchSummary = { ...batch, executionEnd: 'endstring', executionElapsed: 'elapsedstring' }; + const batchCompleteReturn = await trigger(batchComplete, arg => runner.handleBatchComplete(arg), runner.onBatchEnd); + assert.deepStrictEqual(batchCompleteReturn, batchComplete); + // query complete + await trigger([batchComplete], arg => runner.handleQueryComplete(arg), runner.onQueryEnd); + assert(!runner.isExecuting); + assert(runner.hasCompleted); + await runner.disposeQuery(); + }); + + test('does handle inital query failure', async () => { + const instantiationService = workbenchInstantiationService(); + const uri = URI.parse('test:uri').toString(); + const runner = instantiationService.createInstance(QueryRunner, uri); + const runQueryStub = sinon.stub().returns(Promise.reject(new Error('some error'))); + (instantiationService as TestInstantiationService).stub(IQueryManagementService, 'runQuery', runQueryStub); + assert(!runner.isExecuting); + assert(!runner.hasCompleted); + // start query + const queryCompletePromise = Event.toPromise(runner.onQueryEnd); + const rangeSelection = { endColumn: 1, endLineNumber: 1, startColumn: 1, startLineNumber: 1 }; + await runner.runQuery(rangeSelection); + await queryCompletePromise; + assert(runQueryStub.calledOnce); + assert(runQueryStub.calledWithExactly(uri, rangeSelection, undefined)); + assert(runner.messages.length === 2); + assert(runner.messages[0].message.includes('some error')); + assert(runner.messages[0].isError); + }); + + test('does handle cancel query', async () => { + const instantiationService = workbenchInstantiationService(); + const uri = URI.parse('test:uri').toString(); + const runner = instantiationService.createInstance(QueryRunner, uri); + assert(!runner.isExecuting); + // start query + const rangeSelection = { endColumn: 1, endLineNumber: 1, startColumn: 1, startLineNumber: 1 }; + await runner.runQuery(rangeSelection); + assert(runner.isExecuting); + // cancel query + const cancelQueryStub = sinon.stub().returns(Promise.resolve()); + (instantiationService as TestInstantiationService).stub(IQueryManagementService, 'cancelQuery', cancelQueryStub); + await runner.cancelQuery(); + assert(cancelQueryStub.calledOnce); + await trigger([], () => runner.handleQueryComplete(), runner.onQueryEnd); + assert(!runner.isExecuting); + }); + + test('does handle query plan in inital data set', async () => { + const instantiationService = workbenchInstantiationService(); + const uri = URI.parse('test:uri').toString(); + const runner = instantiationService.createInstance(QueryRunner, uri); + const runQueryStub = sinon.stub().returns(Promise.resolve()); + (instantiationService as TestInstantiationService).stub(IQueryManagementService, 'runQuery', runQueryStub); + await runner.runQuery(undefined, { displayEstimatedQueryPlan: true }); + assert(runQueryStub.calledOnce); + assert(runQueryStub.calledWithExactly(uri, undefined, { displayEstimatedQueryPlan: true })); + const xmlPlan = 'xml plan'; + const getRowsStub = sinon.stub().returns(Promise.resolve({ rowCount: 1, rows: [[{ displayValue: xmlPlan }]] } as ResultSetSubset)); + (instantiationService as TestInstantiationService).stub(IQueryManagementService, 'getQueryRows', getRowsStub); + runner.handleBatchStart({ id: 0, executionStart: '' }); + runner.handleResultSetAvailable({ id: 0, batchId: 0, complete: true, rowCount: 1, columnInfo: [{ columnName: 'Microsoft SQL Server 2005 XML Showplan' }] }); + const plan = await runner.planXml; + assert(getRowsStub.calledOnce); + assert.equal(plan, xmlPlan); + assert(runner.isQueryPlan); + }); + + test('does handle query plan in update', async () => { + const instantiationService = workbenchInstantiationService(); + const uri = URI.parse('test:uri').toString(); + const runner = instantiationService.createInstance(QueryRunner, uri); + const runQueryStub = sinon.stub().returns(Promise.resolve()); + (instantiationService as TestInstantiationService).stub(IQueryManagementService, 'runQuery', runQueryStub); + await runner.runQuery(undefined, { displayEstimatedQueryPlan: true }); + assert(runQueryStub.calledOnce); + assert(runQueryStub.calledWithExactly(uri, undefined, { displayEstimatedQueryPlan: true })); + runner.handleBatchStart({ id: 0, executionStart: '' }); + runner.handleResultSetAvailable({ id: 0, batchId: 0, complete: false, rowCount: 0, columnInfo: [{ columnName: 'Microsoft SQL Server 2005 XML Showplan' }] }); + const xmlPlan = 'xml plan'; + const getRowsStub = sinon.stub().returns(Promise.resolve({ rowCount: 1, rows: [[{ displayValue: xmlPlan }]] } as ResultSetSubset)); + (instantiationService as TestInstantiationService).stub(IQueryManagementService, 'getQueryRows', getRowsStub); + runner.handleResultSetUpdated({ id: 0, batchId: 0, complete: true, rowCount: 1, columnInfo: [{ columnName: 'Microsoft SQL Server 2005 XML Showplan' }] }); + const plan = await runner.planXml; + assert(getRowsStub.calledOnce); + assert.equal(plan, xmlPlan); + assert(runner.isQueryPlan); + }); + + test('does run query string', async () => { + const instantiationService = workbenchInstantiationService(); + const uri = URI.parse('test:uri').toString(); + const runner = instantiationService.createInstance(QueryRunner, uri); + const runQueryStringStub = sinon.stub().returns(Promise.resolve()); + (instantiationService as TestInstantiationService).stub(IQueryManagementService, 'runQueryString', runQueryStringStub); + assert(!runner.isExecuting); + assert(!runner.hasCompleted); + // start query + await runner.runQuery('some query'); + assert(runQueryStringStub.calledOnce); + assert(runQueryStringStub.calledWithExactly(uri, 'some query')); + assert(runner.isExecuting); + }); + + test('does handle run query string error', async () => { + const instantiationService = workbenchInstantiationService(); + const uri = URI.parse('test:uri').toString(); + const runner = instantiationService.createInstance(QueryRunner, uri); + const runQueryStringStub = sinon.stub().returns(Promise.reject(new Error('some error'))); + (instantiationService as TestInstantiationService).stub(IQueryManagementService, 'runQueryString', runQueryStringStub); + assert(!runner.isExecuting); + assert(!runner.hasCompleted); + // start query + const queryCompletePromise = Event.toPromise(runner.onQueryEnd); + await runner.runQuery('some query'); + await queryCompletePromise; + assert(runQueryStringStub.calledOnce); + assert(runQueryStringStub.calledWithExactly(uri, 'some query')); + assert(runner.messages.length === 2); + assert(runner.messages[0].message.includes('some error')); + assert(runner.messages[0].isError); + }); + + test('does run query statement', async () => { + const instantiationService = workbenchInstantiationService(); + const uri = URI.parse('test:uri').toString(); + const runner = instantiationService.createInstance(QueryRunner, uri); + const runQueryStatementStub = sinon.stub().returns(Promise.resolve()); + (instantiationService as TestInstantiationService).stub(IQueryManagementService, 'runQueryStatement', runQueryStatementStub); + assert(!runner.isExecuting); + assert(!runner.hasCompleted); + // start query + const rangeSelection = { endColumn: 1, endLineNumber: 1, startColumn: 1, startLineNumber: 1 }; + await runner.runQueryStatement(rangeSelection); + assert(runQueryStatementStub.calledOnce); + assert(runQueryStatementStub.calledWithExactly(uri, rangeSelection.startLineNumber, rangeSelection.startColumn)); + assert(runner.isExecuting); + }); + + test('does handle run query statement error', async () => { + const instantiationService = workbenchInstantiationService(); + const uri = URI.parse('test:uri').toString(); + const runner = instantiationService.createInstance(QueryRunner, uri); + const runQueryStatementStub = sinon.stub().returns(Promise.reject(new Error('some error'))); + (instantiationService as TestInstantiationService).stub(IQueryManagementService, 'runQueryStatement', runQueryStatementStub); + assert(!runner.isExecuting); + assert(!runner.hasCompleted); + // start query + const queryCompletePromise = Event.toPromise(runner.onQueryEnd); + const rangeSelection = { endColumn: 1, endLineNumber: 1, startColumn: 1, startLineNumber: 1 }; + await runner.runQueryStatement(rangeSelection); + await queryCompletePromise; + assert(runQueryStatementStub.calledOnce); + assert(runQueryStatementStub.calledWithExactly(uri, rangeSelection.startLineNumber, rangeSelection.startColumn)); + assert(runner.messages.length === 2); + assert(runner.messages[0].message.includes('some error')); + assert(runner.messages[0].isError); + }); +}); + +function trigger(arg: T, func: (arg: T) => void, event: Event): Promise { + const promise = Event.toPromise(event); + func(arg); + return promise; +} diff --git a/src/sql/workbench/services/query/test/common/testQueryManagementService.ts b/src/sql/workbench/services/query/test/common/testQueryManagementService.ts new file mode 100644 index 0000000000..bb11c35e03 --- /dev/null +++ b/src/sql/workbench/services/query/test/common/testQueryManagementService.ts @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IQueryManagementService, IQueryRequestHandler, ExecutionPlanOptions } from 'sql/workbench/services/query/common/queryManagement'; +import { Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import QueryRunner from 'sql/workbench/services/query/common/queryRunner'; +import * as azdata from 'azdata'; +import { IRange } from 'vs/editor/common/core/range'; +import { ResultSetSubset } from 'sql/workbench/services/query/common/query'; + +export class TestQueryManagementService implements IQueryManagementService { + _serviceBrand: undefined; + onHandlerAdded: Event; + addQueryRequestHandler(queryType: string, runner: IQueryRequestHandler): IDisposable { + throw new Error('Method not implemented.'); + } + isProviderRegistered(providerId: string): boolean { + throw new Error('Method not implemented.'); + } + getRegisteredProviders(): string[] { + throw new Error('Method not implemented.'); + } + registerRunner(runner: QueryRunner, uri: string): void { + return; + } + async cancelQuery(ownerUri: string): Promise { + return { messages: undefined }; + } + async runQuery(ownerUri: string, range: IRange, runOptions?: ExecutionPlanOptions): Promise { + return; + } + runQueryStatement(ownerUri: string, line: number, column: number): Promise { + throw new Error('Method not implemented.'); + } + runQueryString(ownerUri: string, queryString: string): Promise { + throw new Error('Method not implemented.'); + } + runQueryAndReturn(ownerUri: string, queryString: string): Promise { + throw new Error('Method not implemented.'); + } + parseSyntax(ownerUri: string, query: string): Promise { + throw new Error('Method not implemented.'); + } + getQueryRows(rowData: azdata.QueryExecuteSubsetParams): Promise { + throw new Error('Method not implemented.'); + } + async disposeQuery(ownerUri: string): Promise { + return; + } + saveResults(requestParams: azdata.SaveResultsRequestParams): Promise { + throw new Error('Method not implemented.'); + } + setQueryExecutionOptions(uri: string, options: azdata.QueryExecutionOptions): Promise { + throw new Error('Method not implemented.'); + } + onQueryComplete(result: azdata.QueryExecuteCompleteNotificationResult): void { + throw new Error('Method not implemented.'); + } + onBatchStart(batchInfo: azdata.QueryExecuteBatchNotificationParams): void { + throw new Error('Method not implemented.'); + } + onBatchComplete(batchInfo: azdata.QueryExecuteBatchNotificationParams): void { + throw new Error('Method not implemented.'); + } + onResultSetAvailable(resultSetInfo: azdata.QueryExecuteResultSetNotificationParams): void { + throw new Error('Method not implemented.'); + } + onResultSetUpdated(resultSetInfo: azdata.QueryExecuteResultSetNotificationParams): void { + throw new Error('Method not implemented.'); + } + onMessage(message: Map): void { + throw new Error('Method not implemented.'); + } + onEditSessionReady(ownerUri: string, success: boolean, message: string): void { + throw new Error('Method not implemented.'); + } + initializeEdit(ownerUri: string, schemaName: string, objectName: string, objectType: string, rowLimit: number, queryString: string): Promise { + throw new Error('Method not implemented.'); + } + disposeEdit(ownerUri: string): Promise { + throw new Error('Method not implemented.'); + } + updateCell(ownerUri: string, rowId: number, columnId: number, newValue: string): Promise { + throw new Error('Method not implemented.'); + } + commitEdit(ownerUri: string): Promise { + throw new Error('Method not implemented.'); + } + createRow(ownerUri: string): Promise { + throw new Error('Method not implemented.'); + } + deleteRow(ownerUri: string, rowId: number): Promise { + throw new Error('Method not implemented.'); + } + revertCell(ownerUri: string, rowId: number, columnId: number): Promise { + throw new Error('Method not implemented.'); + } + revertRow(ownerUri: string, rowId: number): Promise { + throw new Error('Method not implemented.'); + } + getEditRows(rowData: azdata.EditSubsetParams): Promise { + throw new Error('Method not implemented.'); + } + +} diff --git a/src/sql/workbench/services/query/test/common/testQueryModelService.ts b/src/sql/workbench/services/query/test/common/testQueryModelService.ts index 1278bcb9bc..0480290887 100644 --- a/src/sql/workbench/services/query/test/common/testQueryModelService.ts +++ b/src/sql/workbench/services/query/test/common/testQueryModelService.ts @@ -9,6 +9,7 @@ import * as azdata from 'azdata'; import { Event } from 'vs/base/common/event'; import { QueryInfo } from 'sql/workbench/services/query/common/queryModelService'; import { DataService } from 'sql/workbench/services/query/common/dataService'; +import { IRange } from 'vs/editor/common/core/range'; export class TestQueryModelService implements IQueryModelService { _serviceBrand: any; @@ -25,10 +26,10 @@ export class TestQueryModelService implements IQueryModelService { getQueryRows(uri: string, rowStart: number, numberOfRows: number, batchId: number, resultId: number): Promise { throw new Error('Method not implemented.'); } - runQuery(uri: string, selection: azdata.ISelectionData, runOptions?: azdata.ExecutionPlanOptions): void { + runQuery(uri: string, range: IRange, runOptions?: azdata.ExecutionPlanOptions): void { throw new Error('Method not implemented.'); } - runQueryStatement(uri: string, selection: azdata.ISelectionData): void { + runQueryStatement(uri: string, range: IRange): void { throw new Error('Method not implemented.'); } runQueryString(uri: string, selection: string) { diff --git a/src/sql/workbench/services/queryHistory/common/queryHistoryServiceImpl.ts b/src/sql/workbench/services/queryHistory/common/queryHistoryServiceImpl.ts index 4c87555d6c..f4a13867ec 100644 --- a/src/sql/workbench/services/queryHistory/common/queryHistoryServiceImpl.ts +++ b/src/sql/workbench/services/queryHistory/common/queryHistoryServiceImpl.ts @@ -52,12 +52,12 @@ export class QueryHistoryService extends Disposable implements IQueryHistoryServ const uri: URI = URI.parse(e.uri); // VS Range is 1 based so offset values by 1. The endLine we get back from SqlToolsService is incremented // by 1 from the original input range sent in as well so take that into account and don't modify - const text: string = e.queryInfo.selection && e.queryInfo.selection.length > 0 ? + const text: string = e.queryInfo.range && e.queryInfo.range.length > 0 ? _modelService.getModel(uri).getValueInRange(new Range( - e.queryInfo.selection[0].startLine + 1, - e.queryInfo.selection[0].startColumn + 1, - e.queryInfo.selection[0].endLine, - e.queryInfo.selection[0].endColumn + 1)) : + e.queryInfo.range[0].startLineNumber, + e.queryInfo.range[0].startColumn, + e.queryInfo.range[0].endLineNumber, + e.queryInfo.range[0].endColumn)) : // If no specific selection get the entire text _modelService.getModel(uri).getValue(); diff --git a/src/sql/workbench/test/workbenchTestServices.ts b/src/sql/workbench/test/workbenchTestServices.ts index da9fb4a69b..0d0424c8d1 100644 --- a/src/sql/workbench/test/workbenchTestServices.ts +++ b/src/sql/workbench/test/workbenchTestServices.ts @@ -12,6 +12,8 @@ import { TestObjectExplorerService } from 'sql/workbench/services/objectExplorer import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService'; import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService'; import { TestQueryEditorService } from 'sql/workbench/services/queryEditor/test/common/testQueryEditorService'; +import { IQueryManagementService } from 'sql/workbench/services/query/common/queryManagement'; +import { TestQueryManagementService } from 'sql/workbench/services/query/test/common/testQueryManagementService'; export function workbenchInstantiationService(): ITestInstantiationService { const instantiationService = vsworkbenchInstantiationService(); @@ -19,5 +21,6 @@ export function workbenchInstantiationService(): ITestInstantiationService { instantiationService.stub(IConnectionManagementService, new TestConnectionManagementService()); instantiationService.stub(IQueryModelService, new TestQueryModelService()); instantiationService.stub(IObjectExplorerService, new TestObjectExplorerService()); + instantiationService.stub(IQueryManagementService, new TestQueryManagementService()); return instantiationService; }
${escape(row[columnIndex].displayValue)}