diff --git a/extensions/import/src/test/utils.test.ts b/extensions/import/src/test/utils.test.ts index d31cec47eb..e3cb1bd0d4 100644 --- a/extensions/import/src/test/utils.test.ts +++ b/extensions/import/src/test/utils.test.ts @@ -72,6 +72,9 @@ export class TestQueryProvider implements azdata.QueryProvider { disposeQuery(ownerUri: string): Thenable { throw new Error('Method not implemented.'); } + connectionUriChanged(newUri: string, oldUri: string): Thenable { + throw new Error('Method not implemented.'); + } saveResults(requestParams: azdata.SaveResultsRequestParams): Thenable { throw new Error('Method not implemented.'); } diff --git a/extensions/liveshare/src/constants.ts b/extensions/liveshare/src/constants.ts index 0fd74e110b..c58c8abd86 100644 --- a/extensions/liveshare/src/constants.ts +++ b/extensions/liveshare/src/constants.ts @@ -33,5 +33,6 @@ export const runQueryAndReturnRequest = 'runQueryAndReturn'; export const parseSyntaxRequest = 'parseSyntax'; export const getQueryRowsRequest = 'getQueryRows'; export const disposeQueryRequest = 'disposeQuery'; +export const connectionUriChangedNotification = 'connectionUriChanged'; export const setQueryExecutionOptionsRequest = 'setQueryExecutionOptions'; export const saveResultsRequest = 'saveResultsRequest'; diff --git a/extensions/liveshare/src/providers/queryProvider.ts b/extensions/liveshare/src/providers/queryProvider.ts index 88cd4e80a7..e786275dcf 100644 --- a/extensions/liveshare/src/providers/queryProvider.ts +++ b/extensions/liveshare/src/providers/queryProvider.ts @@ -59,6 +59,10 @@ export class QueryProvider { return true; }); + this._sharedService.onNotify(constants.connectionUriChangedNotification, (args: any) => { + return true; + }); + this._sharedService.onRequest(constants.saveResultsRequest, (args: any) => { return true; }); @@ -132,6 +136,13 @@ export class QueryProvider { }]); }; + let connectionUriChanged = (ownerUri: string): Thenable => { + self._sharedServiceProxy.notify(constants.connectionUriChangedNotification, [{ + ownerUri: ownerUri + }]); + return Promise.resolve(); + }; + let registerOnQueryComplete = (handler: (result: azdata.QueryExecuteCompleteNotificationResult) => any): void => { self._onQueryCompleteHandler = handler; }; @@ -208,6 +219,7 @@ export class QueryProvider { deleteRow, disposeEdit, disposeQuery, + connectionUriChanged, getEditRows, getQueryRows, setQueryExecutionOptions, diff --git a/extensions/machine-learning/src/test/queryRunner.test.ts b/extensions/machine-learning/src/test/queryRunner.test.ts index d2c3f9e61b..56ce412720 100644 --- a/extensions/machine-learning/src/test/queryRunner.test.ts +++ b/extensions/machine-learning/src/test/queryRunner.test.ts @@ -29,6 +29,7 @@ function createContext(): TestContext { parseSyntax: () => { return Promise.reject(); }, getQueryRows: () => { return Promise.reject(); }, disposeQuery: () => { return Promise.reject(); }, + connectionUriChanged: () => { return Promise.reject(); }, saveResults: () => { return Promise.reject(); }, setQueryExecutionOptions: () => { return Promise.reject(); }, registerOnQueryComplete: () => { return Promise.reject(); }, diff --git a/extensions/mssql/config.json b/extensions/mssql/config.json index d092d72d6a..e9daf7edf3 100644 --- a/extensions/mssql/config.json +++ b/extensions/mssql/config.json @@ -1,6 +1,6 @@ { "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", - "version": "3.0.0-release.126", + "version": "3.0.0-release.127", "downloadFileNames": { "Windows_86": "win-x86-net5.0.zip", "Windows_64": "win-x64-net5.0.zip", diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index 6bc8afc008..a59d81028d 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -1259,7 +1259,7 @@ "@microsoft/ads-kerberos": "^1.1.3", "buffer-stream-reader": "^0.1.1", "bytes": "^3.1.0", - "dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#1.2.3", + "dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#1.2.4", "error-ex": "^1.3.2", "figures": "^2.0.0", "find-remove": "1.2.1", diff --git a/extensions/mssql/yarn.lock b/extensions/mssql/yarn.lock index 0faa940716..3f11a337e6 100644 --- a/extensions/mssql/yarn.lock +++ b/extensions/mssql/yarn.lock @@ -529,9 +529,9 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#1.2.3": - version "1.2.3" - resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/c2282e6230e596eb647be8d50239b0486707f962" +"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#1.2.4": + version "1.2.4" + resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/3fbb5cebd29958f07d450fd9b1a969faf489c6a8" dependencies: vscode-languageclient "5.2.1" diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 7ca20e36aa..1ad31796d3 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -948,6 +948,13 @@ declare module 'azdata' { parentTypeName?: string; } + export interface QueryProvider { + /** + * Notify clients that the URI for a connection has been changed. + */ + connectionUriChanged(newUri: string, oldUri: string): Thenable; + } + export namespace accounts { export interface AccountSecurityToken { /** diff --git a/src/sql/base/query/browser/untitledQueryEditorInput.ts b/src/sql/base/query/browser/untitledQueryEditorInput.ts index 94dcf780a2..71264d6c40 100644 --- a/src/sql/base/query/browser/untitledQueryEditorInput.ts +++ b/src/sql/base/query/browser/untitledQueryEditorInput.ts @@ -13,7 +13,10 @@ import { IResolvedTextEditorModel } from 'vs/editor/common/services/resolverServ import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { EncodingMode } from 'vs/workbench/services/textfile/common/textfiles'; -import { EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { GroupIdentifier, ISaveOptions, IEditorInput, EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { FileQueryEditorInput } from 'sql/workbench/contrib/query/browser/fileQueryEditorInput'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { UNTITLED_QUERY_EDITOR_TYPEID } from 'sql/workbench/common/constants'; import { IUntitledQueryEditorInput } from 'sql/base/query/common/untitledQueryEditorInput'; @@ -27,9 +30,10 @@ export class UntitledQueryEditorInput extends QueryEditorInput implements IUntit results: QueryResultsInput, @IConnectionManagementService connectionManagementService: IConnectionManagementService, @IQueryModelService queryModelService: IQueryModelService, - @IConfigurationService configurationService: IConfigurationService + @IConfigurationService configurationService: IConfigurationService, + @IInstantiationService instantiationService: IInstantiationService, ) { - super(description, text, results, connectionManagementService, queryModelService, configurationService); + super(description, text, results, connectionManagementService, queryModelService, configurationService, instantiationService); } public override resolve(): Promise { @@ -44,6 +48,37 @@ export class UntitledQueryEditorInput extends QueryEditorInput implements IUntit return this.text.model.hasAssociatedFilePath; } + override async save(group: GroupIdentifier, options?: ISaveOptions): Promise { + let fileEditorInput = await this.text.save(group, options); + return this.createFileQueryEditorInput(fileEditorInput); + } + + override async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { + let fileEditorInput = await this.text.saveAs(group, options); + return this.createFileQueryEditorInput(fileEditorInput); + } + + private async createFileQueryEditorInput(fileEditorInput: IEditorInput): Promise { + // Create our own FileQueryEditorInput wrapper here so that the existing state (connection, results, etc) can be transferred from this input to the new file input. + try { + let newUri = fileEditorInput.resource.toString(true); + await this.changeConnectionUri(newUri); + this._results.uri = newUri; + let newInput = this.instantiationService.createInstance(FileQueryEditorInput, '', (fileEditorInput as FileEditorInput), this.results); + newInput.state.setState(this.state); + return newInput; + } + catch (error) { + /** + * We are saving over a file that is already open and connected, return the unaltered save editor input directly (to avoid side effects when changing connection). + * This will replace the editor input in the already open window with that of the new one, + * the connection will be undefined and the file appears to be disconnected even if its not, change the connection to fix this. + * also the results shown will be the old results, until run is clicked again. + */ + return fileEditorInput; + } + } + public setMode(mode: string): void { this.text.setMode(mode); } diff --git a/src/sql/platform/connection/common/connectionManagement.ts b/src/sql/platform/connection/common/connectionManagement.ts index 6b9778926d..10b20ec2cd 100644 --- a/src/sql/platform/connection/common/connectionManagement.ts +++ b/src/sql/platform/connection/common/connectionManagement.ts @@ -119,6 +119,11 @@ export interface IConnectionManagementService { */ connectAndSaveProfile(connection: IConnectionProfile, uri: string, options?: IConnectionCompletionOptions, callbacks?: IConnectionCallbacks): Promise; + /** + * Replaces a connectioninfo's associated uri with a new uri. + */ + changeConnectionUri(newUri: string, oldUri: string): void + /** * Finds existing connection for given profile and purpose is any exists. * The purpose is connection by default diff --git a/src/sql/platform/connection/common/connectionStatusManager.ts b/src/sql/platform/connection/common/connectionStatusManager.ts index 2ea5ba3718..f161b3f630 100644 --- a/src/sql/platform/connection/common/connectionStatusManager.ts +++ b/src/sql/platform/connection/common/connectionStatusManager.ts @@ -13,6 +13,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { join } from 'vs/base/common/path'; import * as Utils from 'sql/platform/connection/common/utils'; import * as azdata from 'azdata'; +import * as nls from 'vs/nls'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { values } from 'vs/base/common/collections'; @@ -49,6 +50,21 @@ export class ConnectionStatusManager { return !!this.findConnection(id); } + public changeConnectionUri(newUri: string, oldUri: string): void { + let info = this.findConnection(oldUri); + if (!info) { + this._logService.error(`No connection found associated with old URI : '${oldUri}'`); + throw new Error(nls.localize('connectionStatusManager.noConnectionForUri', 'Could not find connection with uri: {0}', oldUri)); + } + if (this._connections[newUri]) { + this._logService.error(`New URI : '${newUri}' is already in the connections list.`); + throw new Error(nls.localize('connectionStatusManager.uriAlreadyInConnectionsList', 'There is already a connection with uri: {0}', newUri)); + } + info.ownerUri = newUri; + this._connections[newUri] = info; + delete this._connections[oldUri]; + } + public deleteConnection(id: string): void { let info = this.findConnection(id); if (info) { diff --git a/src/sql/platform/connection/test/common/testConnectionManagementService.ts b/src/sql/platform/connection/test/common/testConnectionManagementService.ts index 84c56702fd..ce8c9b5e16 100644 --- a/src/sql/platform/connection/test/common/testConnectionManagementService.ts +++ b/src/sql/platform/connection/test/common/testConnectionManagementService.ts @@ -50,6 +50,10 @@ export class TestConnectionManagementService implements IConnectionManagementSer } + changeConnectionUri(newUri: string, oldUri: string): void { + + } + showConnectionDialog(params?: INewConnectionParams, options?: IConnectionCompletionOptions, model?: IConnectionProfile, connectionResult?: IConnectionResult): Promise { return undefined!; } diff --git a/src/sql/workbench/api/browser/mainThreadDataProtocol.ts b/src/sql/workbench/api/browser/mainThreadDataProtocol.ts index 05249311bd..f6a4386cb6 100644 --- a/src/sql/workbench/api/browser/mainThreadDataProtocol.ts +++ b/src/sql/workbench/api/browser/mainThreadDataProtocol.ts @@ -132,6 +132,9 @@ export class MainThreadDataProtocol extends Disposable implements MainThreadData disposeQuery(ownerUri: string): Promise { return Promise.resolve(self._proxy.$disposeQuery(handle, ownerUri)); }, + connectionUriChanged(newUri: string, oldUri: string): Promise { + return Promise.resolve(self._proxy.$connectionUriChanged(handle, newUri, oldUri)); + }, saveResults(requestParams: azdata.SaveResultsRequestParams): Promise { let saveResultsFeatureInfo = self._serializationService.getSaveResultsFeatureMetadataProvider(requestParams.ownerUri); if (saveResultsFeatureInfo && saveResultsFeatureInfo.enabled) { diff --git a/src/sql/workbench/api/common/extHostDataProtocol.ts b/src/sql/workbench/api/common/extHostDataProtocol.ts index c98512a3bd..d63d78a7c7 100644 --- a/src/sql/workbench/api/common/extHostDataProtocol.ts +++ b/src/sql/workbench/api/common/extHostDataProtocol.ts @@ -316,6 +316,14 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape { return this._resolveProvider(handle).disposeQuery(ownerUri); } + override $connectionUriChanged(handle: number, newUri: string, oldUri: string): Thenable { + if (this.uriTransformer) { + newUri = this._getTransformedUri(newUri, this.uriTransformer.transformOutgoing); + oldUri = this._getTransformedUri(oldUri, this.uriTransformer.transformOutgoing); + } + return this._resolveProvider(handle).connectionUriChanged(newUri, oldUri); + } + override $onQueryComplete(handle: number, result: azdata.QueryExecuteCompleteNotificationResult): void { if (this.uriTransformer) { result.ownerUri = this._getTransformedUri(result.ownerUri, this.uriTransformer.transformOutgoing); diff --git a/src/sql/workbench/api/common/sqlExtHost.protocol.ts b/src/sql/workbench/api/common/sqlExtHost.protocol.ts index 8e36424923..3ec3c186fd 100644 --- a/src/sql/workbench/api/common/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/common/sqlExtHost.protocol.ts @@ -193,6 +193,11 @@ export abstract class ExtHostDataProtocolShape { */ $disposeQuery(handle: number, ownerUri: string): Thenable { throw ni(); } + /** + * Notifies the client that the URI associated with a connection has changed + */ + $connectionUriChanged(handle: number, newUri: string, oldUri: string): Thenable { throw ni(); } + /** * Refreshes the IntelliSense cache */ diff --git a/src/sql/workbench/common/editor/query/queryEditorInput.ts b/src/sql/workbench/common/editor/query/queryEditorInput.ts index 64d0462517..edf75078de 100644 --- a/src/sql/workbench/common/editor/query/queryEditorInput.ts +++ b/src/sql/workbench/common/editor/query/queryEditorInput.ts @@ -19,6 +19,7 @@ import { IRange } from 'vs/editor/common/core/range'; import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { IQueryEditorConfiguration } from 'sql/platform/query/common/query'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; const MAX_SIZE = 13; @@ -106,6 +107,14 @@ export class QueryEditorState extends Disposable { public get isSqlCmdMode(): boolean { return this._isSqlCmdMode; } + + public setState(newState: QueryEditorState): void { + this.connected = newState.connected; + this.connecting = newState.connecting; + this.resultsVisible = newState.resultsVisible; + this.executing = newState.executing; + this.isSqlCmdMode = newState.isSqlCmdMode; + } } /** @@ -125,7 +134,8 @@ export abstract class QueryEditorInput extends EditorInput implements IConnectab protected _results: QueryResultsInput, @IConnectionManagementService private readonly connectionManagementService: IConnectionManagementService, @IQueryModelService private readonly queryModelService: IQueryModelService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IInstantiationService protected readonly instantiationService: IInstantiationService ) { super(); @@ -196,6 +206,17 @@ export abstract class QueryEditorInput extends EditorInput implements IConnectab } } + protected async changeConnectionUri(newUri: string): Promise { + this.connectionManagementService.changeConnectionUri(newUri, this.uri); + try { + await this.queryModelService.changeConnectionUri(newUri, this.uri); + } + catch (error) { + this.connectionManagementService.changeConnectionUri(this.uri, newUri); + throw error; + } + } + // Forwarding resource functions to the inline sql file editor public override isDirty(): boolean { return this._text.isDirty(); } public get resource(): URI { return this._text.resource; } @@ -226,10 +247,6 @@ export abstract class QueryEditorInput extends EditorInput implements IConnectab return this.text.save(group, options); } - override saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { - return this.text.saveAs(group, options); - } - // Called to get the tooltip of the tab public override getTitle(): string { return this.getName(true); diff --git a/src/sql/workbench/common/editor/query/queryResultsInput.ts b/src/sql/workbench/common/editor/query/queryResultsInput.ts index 4232845b3c..14fc022e62 100644 --- a/src/sql/workbench/common/editor/query/queryResultsInput.ts +++ b/src/sql/workbench/common/editor/query/queryResultsInput.ts @@ -46,7 +46,7 @@ export class QueryResultsInput extends EditorInput { return this._state; } - constructor(private _uri: string) { + constructor(public uri: string) { super(); } @@ -60,7 +60,7 @@ export class QueryResultsInput extends EditorInput { override matches(other: any): boolean { if (other instanceof QueryResultsInput) { - return (other._uri === this._uri); + return (other.uri === this.uri); } return false; @@ -84,10 +84,6 @@ export class QueryResultsInput extends EditorInput { return 'workbench.query.queryResultsInput'; } - get uri(): string { - return this._uri; - } - get resource(): URI | undefined { return undefined; } diff --git a/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts b/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts index e9b4c16c23..ba4648cf4e 100644 --- a/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts +++ b/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts @@ -395,7 +395,7 @@ suite('commandLineService tests', () => { let uri = URI.file(args._[0]); const workbenchinstantiationService = workbenchInstantiationService(); const editorInput = workbenchinstantiationService.createInstance(FileEditorInput, uri, undefined, undefined, undefined, undefined, undefined, undefined); - const queryInput = new FileQueryEditorInput(undefined, editorInput, undefined, connectionManagementService.object, querymodelService.object, configurationService.object); + const queryInput = new FileQueryEditorInput(undefined, editorInput, undefined, connectionManagementService.object, querymodelService.object, configurationService.object, workbenchinstantiationService); queryInput.state.connected = true; const editorService: TypeMoq.Mock = TypeMoq.Mock.ofType(TestEditorService, TypeMoq.MockBehavior.Strict); editorService.setup(e => e.editors).returns(() => [queryInput]); diff --git a/src/sql/workbench/contrib/query/browser/fileQueryEditorInput.ts b/src/sql/workbench/contrib/query/browser/fileQueryEditorInput.ts index 8712f3e6bc..d30eb70f14 100644 --- a/src/sql/workbench/contrib/query/browser/fileQueryEditorInput.ts +++ b/src/sql/workbench/contrib/query/browser/fileQueryEditorInput.ts @@ -10,11 +10,12 @@ import { IQueryModelService } from 'sql/workbench/services/query/common/queryMod import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IMoveResult, GroupIdentifier } from 'vs/workbench/common/editor'; +import { IMoveResult, GroupIdentifier, ISaveOptions, IEditorInput } from 'vs/workbench/common/editor'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { EncodingMode, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { URI } from 'vs/base/common/uri'; import { FILE_QUERY_EDITOR_TYPEID } from 'sql/workbench/common/constants'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export class FileQueryEditorInput extends QueryEditorInput { @@ -26,9 +27,10 @@ export class FileQueryEditorInput extends QueryEditorInput { results: QueryResultsInput, @IConnectionManagementService connectionManagementService: IConnectionManagementService, @IQueryModelService queryModelService: IQueryModelService, - @IConfigurationService configurationService: IConfigurationService + @IConfigurationService configurationService: IConfigurationService, + @IInstantiationService instantiationService: IInstantiationService ) { - super(description, text, results, connectionManagementService, queryModelService, configurationService); + super(description, text, results, connectionManagementService, queryModelService, configurationService, instantiationService); } public override resolve(): Promise { @@ -86,4 +88,35 @@ export class FileQueryEditorInput extends QueryEditorInput { public override rename(group: GroupIdentifier, target: URI): IMoveResult { return this.text.rename(group, target); } + + override async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { + let newEditorInput = await this.text.saveAs(group, options); + let newUri = newEditorInput.resource.toString(true); + if (newUri === this.uri) { + // URI is the same location, no need to change URI for the query in services, just return input. + return newEditorInput; + } + else { + // URI is different, need to update URI for the query in services to ensure we can keep the current results/view state + // without resetting and creating a brand new query. + try { + await this.changeConnectionUri(newUri); + this._results.uri = newUri; + // Create a new FileQueryEditorInput with current results and state in order to trigger a rename for editor tab name. + // (Tab name won't refresh automatically if current input is reused directly) + let newFileQueryInput = this.instantiationService.createInstance(FileQueryEditorInput, '', (newEditorInput as FileEditorInput), this.results); + newFileQueryInput.state.setState(this.state); + return newFileQueryInput; + } + catch (error) { + /** + * We are saving over a file that is already open and connected, return the unaltered save editor input directly (to avoid side effects when changing connection). + * This will replace the editor input in the already open window with that of the new one, + * the connection will be undefined and the file appears to be disconnected even if its not, change the connection to fix this. + * also the results shown will be the old results, until run is clicked again. + */ + return newEditorInput; + } + } + } } diff --git a/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts b/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts index f748fe44e7..fc04285301 100644 --- a/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts +++ b/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts @@ -313,7 +313,8 @@ suite('SQL QueryEditor Tests', () => { undefined, connectionManagementService.object, queryModelService.object, - configurationService.object + configurationService.object, + testinstantiationService ); }); diff --git a/src/sql/workbench/services/connection/browser/connectionManagementService.ts b/src/sql/workbench/services/connection/browser/connectionManagementService.ts index 52e9530f57..1037c92fbe 100644 --- a/src/sql/workbench/services/connection/browser/connectionManagementService.ts +++ b/src/sql/workbench/services/connection/browser/connectionManagementService.ts @@ -1127,6 +1127,20 @@ export class ConnectionManagementService extends Disposable implements IConnecti return Promise.resolve(true); } + /** + * Replaces connection info uri with new uri. + */ + public changeConnectionUri(newUri: string, oldUri: string): void { + this._connectionStatusManager.changeConnectionUri(newUri, oldUri); + if (!this._uriToProvider[oldUri]) { + this._logService.error(`No provider found for old URI : '${oldUri}'`); + throw new Error(nls.localize('connectionManagementService.noProviderForUri', 'Could not find provider for uri: {0}', oldUri)); + } + // Provider will persist after disconnect, it is okay to overwrite the map if it exists from a previously deleted connection. + this._uriToProvider[newUri] = this._uriToProvider[oldUri]; + delete this._uriToProvider[oldUri]; + } + /** * Functions to handle the connecting life cycle */ diff --git a/src/sql/workbench/services/query/common/dataService.ts b/src/sql/workbench/services/query/common/dataService.ts index a44c7200ae..d7d660e261 100644 --- a/src/sql/workbench/services/query/common/dataService.ts +++ b/src/sql/workbench/services/query/common/dataService.ts @@ -31,7 +31,7 @@ export class DataService { private editQueue: Promise; constructor( - private _uri: string, + public uri: string, @IInstantiationService private _instantiationService: IInstantiationService, @IQueryModelService private _queryModel: IQueryModelService ) { @@ -45,13 +45,13 @@ export class DataService { * @param numberOfRows The maximum number of rows to return */ getEditRows(rowStart: number, numberOfRows: number): Promise { - return this._queryModel.getEditRows(this._uri, rowStart, numberOfRows); + return this._queryModel.getEditRows(this.uri, rowStart, numberOfRows); } updateCell(rowId: number, columnId: number, newValue: string): Thenable { const self = this; self.editQueue = self.editQueue.then(() => { - return self._queryModel.updateCell(self._uri, rowId, columnId, newValue).then(result => { + return self._queryModel.updateCell(self.uri, rowId, columnId, newValue).then(result => { return result; }, error => { // Start our editQueue over due to the rejected promise @@ -65,7 +65,7 @@ export class DataService { commitEdit(): Thenable { const self = this; self.editQueue = self.editQueue.then(() => { - return self._queryModel.commitEdit(self._uri).then(result => { + return self._queryModel.commitEdit(self.uri).then(result => { return result; }, error => { // Start our editQueue over due to the rejected promise @@ -79,7 +79,7 @@ export class DataService { createRow(): Thenable { const self = this; self.editQueue = self.editQueue.then(() => { - return self._queryModel.createRow(self._uri).then(result => { + return self._queryModel.createRow(self.uri).then(result => { return result; }, error => { // Start our editQueue over due to the rejected promise @@ -93,7 +93,7 @@ export class DataService { deleteRow(rowId: number): Thenable { const self = this; self.editQueue = self.editQueue.then(() => { - return self._queryModel.deleteRow(self._uri, rowId).then(result => { + return self._queryModel.deleteRow(self.uri, rowId).then(result => { return result; }, error => { // Start our editQueue over due to the rejected promise @@ -108,7 +108,7 @@ export class DataService { revertCell(rowId: number, columnId: number): Thenable { const self = this; self.editQueue = self.editQueue.then(() => { - return self._queryModel.revertCell(self._uri, rowId, columnId).then(result => { + return self._queryModel.revertCell(self.uri, rowId, columnId).then(result => { return result; }, error => { // Start our editQueue over due to the rejected promise @@ -122,7 +122,7 @@ export class DataService { revertRow(rowId: number): Thenable { const self = this; self.editQueue = self.editQueue.then(() => { - return self._queryModel.revertRow(self._uri, rowId).then(result => { + return self._queryModel.revertRow(self.uri, rowId).then(result => { return result; }, error => { // Start our editQueue over due to the rejected promise @@ -141,7 +141,7 @@ export class DataService { */ sendSaveRequest(saveRequest: ISaveRequest): void { let serializer = this._instantiationService.createInstance(ResultSerializer); - serializer.saveResults(this._uri, saveRequest); + serializer.saveResults(this.uri, saveRequest); } /** @@ -152,10 +152,10 @@ export class DataService { * @param includeHeaders [Optional]: Should column headers be included in the copy selection */ copyResults(selection: Slick.Range[], batchId: number, resultId: number, includeHeaders?: boolean): void { - this._queryModel.copyResults(this._uri, selection, batchId, resultId, includeHeaders); + this._queryModel.copyResults(this.uri, selection, batchId, resultId, includeHeaders); } onLoaded(): void { - this._queryModel.onLoaded(this._uri); + this._queryModel.onLoaded(this.uri); } } diff --git a/src/sql/workbench/services/query/common/queryManagement.ts b/src/sql/workbench/services/query/common/queryManagement.ts index 0f71d562ce..5b7e662cc3 100644 --- a/src/sql/workbench/services/query/common/queryManagement.ts +++ b/src/sql/workbench/services/query/common/queryManagement.ts @@ -15,6 +15,8 @@ import EditQueryRunner from 'sql/workbench/services/editData/common/editQueryRun 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'; +import { ILogService } from 'vs/platform/log/common/log'; +import * as nls from 'vs/nls'; export const SERVICE_ID = 'queryManagementService'; @@ -48,6 +50,7 @@ export interface IQueryManagementService { parseSyntax(ownerUri: string, query: string): Promise; getQueryRows(rowData: azdata.QueryExecuteSubsetParams): Promise; disposeQuery(ownerUri: string): Promise; + changeConnectionUri(newUri: string, oldUri: string): Promise; saveResults(requestParams: azdata.SaveResultsRequestParams): Promise; setQueryExecutionOptions(uri: string, options: azdata.QueryExecutionOptions): Promise; @@ -86,6 +89,7 @@ export interface IQueryRequestHandler { parseSyntax(ownerUri: string, query: string): Promise; getQueryRows(rowData: azdata.QueryExecuteSubsetParams): Promise; disposeQuery(ownerUri: string): Promise; + connectionUriChanged(newUri: string, oldUri: string): Promise; saveResults(requestParams: azdata.SaveResultsRequestParams): Promise; setQueryExecutionOptions(ownerUri: string, options: azdata.QueryExecutionOptions): Promise; @@ -114,7 +118,8 @@ export class QueryManagementService implements IQueryManagementService { constructor( @IConnectionManagementService private _connectionService: IConnectionManagementService, - @IAdsTelemetryService private _telemetryService: IAdsTelemetryService + @IAdsTelemetryService private _telemetryService: IAdsTelemetryService, + @ILogService private _logService: ILogService ) { } @@ -266,6 +271,23 @@ export class QueryManagementService implements IQueryManagementService { }); } + public changeConnectionUri(newUri: string, oldUri: string): Promise { + let item = this._queryRunners.get(oldUri); + if (!item) { + this._logService.error(`No query runner found for old URI : '${oldUri}'`); + throw new Error(nls.localize('queryManagement.noQueryRunnerForUri', 'Could not find Query Runner for uri: {0}', oldUri)); + } + if (this._queryRunners.get(newUri)) { + this._logService.error(`New URI : '${newUri}' already has a query runner.`); + throw new Error(nls.localize('queryManagement.uriAlreadyHasQueryRunner', 'Uri: {0} unexpectedly already has a query runner.', newUri)); + } + this._queryRunners.set(newUri, item); + this._queryRunners.delete(oldUri); + return this._runAction(newUri, (runner) => { + return runner.connectionUriChanged(newUri, oldUri); + }); + } + public setQueryExecutionOptions(ownerUri: string, options: azdata.QueryExecutionOptions): Promise { return this._runAction(ownerUri, (runner) => { return runner.setQueryExecutionOptions(ownerUri, options); diff --git a/src/sql/workbench/services/query/common/queryModel.ts b/src/sql/workbench/services/query/common/queryModel.ts index b557425ad2..374998cdd1 100644 --- a/src/sql/workbench/services/query/common/queryModel.ts +++ b/src/sql/workbench/services/query/common/queryModel.ts @@ -59,6 +59,7 @@ export interface IQueryModelService { runQueryString(uri: string, selection: string | undefined): void; cancelQuery(input: QueryRunner | string): void; disposeQuery(uri: string): void; + changeConnectionUri(newUri: string, oldUri: string); isRunningQuery(uri: string): boolean; getDataService(uri: string): DataService; diff --git a/src/sql/workbench/services/query/common/queryModelService.ts b/src/sql/workbench/services/query/common/queryModelService.ts index 559bce76b2..9b38a5fa4e 100644 --- a/src/sql/workbench/services/query/common/queryModelService.ts +++ b/src/sql/workbench/services/query/common/queryModelService.ts @@ -13,6 +13,7 @@ import * as azdata from 'azdata'; import * as nls from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; import { Event, Emitter } from 'vs/base/common/event'; import * as strings from 'vs/base/common/strings'; import * as types from 'vs/base/common/types'; @@ -47,6 +48,11 @@ export class QueryInfo { this.queryEventQueue = []; this.range = []; } + + public set uri(newUri: string) { + this.queryRunner.uri = newUri; + this.dataService.uri = newUri; + } } /** @@ -75,7 +81,8 @@ export class QueryModelService implements IQueryModelService { // CONSTRUCTOR ///////////////////////////////////////////////////////// constructor( @IInstantiationService private _instantiationService: IInstantiationService, - @INotificationService private _notificationService: INotificationService + @INotificationService private _notificationService: INotificationService, + @ILogService private _logService: ILogService ) { this._queryInfoMap = new Map(); this._onRunQueryStart = new Emitter(); @@ -409,6 +416,27 @@ export class QueryModelService implements IQueryModelService { } } + public async changeConnectionUri(newUri: string, oldUri: string): Promise { + // Get existing query runner + let queryRunner = this.internalGetQueryRunner(oldUri); + if (!queryRunner) { + this._logService.error(`A Query and QueryRunner was not found for '${oldUri}'`); + throw new Error(nls.localize('queryModelService.noQueryFoundForUri', 'No Query found for {0}', oldUri)); + } + else if (this._queryInfoMap.has(newUri)) { + this._logService.error(`New URI '${newUri}' already has query info associated with it.`); + throw new Error(nls.localize('queryModelService.uriAlreadyHasQuery', '{0} already has an existing query', newUri)); + } + + await queryRunner.changeConnectionUri(newUri, oldUri); + + // remove the old key and set new key with same query info as old uri. (Info existence is checked in internalGetQueryRunner) + let info = this._queryInfoMap.get(oldUri); + info.uri = newUri; + this._queryInfoMap.set(newUri, info); + this._queryInfoMap.delete(oldUri); + } + // 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 diff --git a/src/sql/workbench/services/query/common/queryRunner.ts b/src/sql/workbench/services/query/common/queryRunner.ts index 7e6ecebe66..bf4031bf55 100644 --- a/src/sql/workbench/services/query/common/queryRunner.ts +++ b/src/sql/workbench/services/query/common/queryRunner.ts @@ -421,6 +421,10 @@ export default class QueryRunner extends Disposable { super.dispose(); } + public changeConnectionUri(oldUri: string, newUri: string): Promise { + return this.queryManagementService.changeConnectionUri(oldUri, newUri); + } + get totalElapsedMilliseconds(): number { return this._totalElapsedMilliseconds; } diff --git a/src/sql/workbench/services/query/test/common/testQueryManagementService.ts b/src/sql/workbench/services/query/test/common/testQueryManagementService.ts index 098b6a113b..83207ef0e5 100644 --- a/src/sql/workbench/services/query/test/common/testQueryManagementService.ts +++ b/src/sql/workbench/services/query/test/common/testQueryManagementService.ts @@ -53,6 +53,9 @@ export class TestQueryManagementService implements IQueryManagementService { async disposeQuery(ownerUri: string): Promise { return; } + changeConnectionUri(newUri: string, oldUri: string): Promise { + throw new Error('Method not implemented.'); + } saveResults(requestParams: azdata.SaveResultsRequestParams): 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 73fbd181b0..8f9fb92ca0 100644 --- a/src/sql/workbench/services/query/test/common/testQueryModelService.ts +++ b/src/sql/workbench/services/query/test/common/testQueryModelService.ts @@ -45,6 +45,9 @@ export class TestQueryModelService implements IQueryModelService { disposeQuery(uri: string): void { throw new Error('Method not implemented.'); } + changeConnectionUri(newUri: string, oldUri: string): void { + throw new Error('Method not implemented.'); + } isRunningQuery(uri: string): boolean { throw new Error('Method not implemented.'); }