diff --git a/extensions/integration-tests/src/schemaCompare.test.ts b/extensions/integration-tests/src/schemaCompare.test.ts index 97dc4a473c..14e9b560a9 100644 --- a/extensions/integration-tests/src/schemaCompare.test.ts +++ b/extensions/integration-tests/src/schemaCompare.test.ts @@ -12,12 +12,14 @@ const path = require('path'); import { context } from './testContext'; import assert = require('assert'); import { getStandaloneServer } from './testConfig'; +import { stressify } from 'adstest'; let schemaCompareService: azdata.SchemaCompareServicesProvider; +let schemaCompareTester: SchemaCompareTester; let dacpac1: string = path.join(__dirname, 'testData/Database1.dacpac'); let dacpac2: string = path.join(__dirname, 'testData/Database2.dacpac'); -let dummyDBName: string = 'ads_schemaCompareDB'; // This is used as fill in name and not created anywhere const SERVER_CONNECTION_TIMEOUT: number = 3000; +const retryCount = 24; // 2 minutes if (context.RunTest) { suite('Schema compare integration test suite', () => { @@ -31,152 +33,187 @@ if (context.RunTest) { attempts--; await utils.sleep(1000); // To ensure the providers are registered. } + schemaCompareTester = new SchemaCompareTester(); console.log(`Start schema compare tests`); }); test('Schema compare dacpac to dacpac comparison', async function () { + await schemaCompareTester.SchemaCompareDacpacToDacpac(); + }); + test('Schema compare database to database comparison and script generation', async function () { + await schemaCompareTester.SchemaCompareDatabaseToDatabase(); + }); + test('Schema compare dacpac to database comparison and script generation', async function () { + await schemaCompareTester.SchemaCompareDacpacToDatabase(); + }); + }); +} + +class SchemaCompareTester { + private static ParallelCount = 1; + + @stressify({ dop: SchemaCompareTester.ParallelCount }) + async SchemaCompareDacpacToDacpac(): Promise { + assert(schemaCompareService, 'Schema Compare Service Provider is not available'); + const now = new Date(); + const operationId = 'testOperationId_' + now.getTime().toString(); + + let source: azdata.SchemaCompareEndpointInfo = { + endpointType: azdata.SchemaCompareEndpointType.Dacpac, + packageFilePath: dacpac1, + serverDisplayName: '', + serverName: '', + databaseName: '', + ownerUri: '', + }; + let target: azdata.SchemaCompareEndpointInfo = { + endpointType: azdata.SchemaCompareEndpointType.Dacpac, + packageFilePath: dacpac2, + serverDisplayName: '', + serverName: '', + databaseName: '', + ownerUri: '', + }; + + let schemaCompareResult = await schemaCompareService.schemaCompare(operationId, source, target, azdata.TaskExecutionMode.execute, null); + this.assertSchemaCompareResult(schemaCompareResult, operationId); + } + + @stressify({ dop: SchemaCompareTester.ParallelCount }) + async SchemaCompareDatabaseToDatabase(): Promise { + let server = await getStandaloneServer(); + await utils.connectToServer(server, SERVER_CONNECTION_TIMEOUT); + + let nodes = await azdata.objectexplorer.getActiveConnectionNodes(); + assert(nodes.length > 0, `Expecting at least one active connection, actual: ${nodes.length}`); + + let index = nodes.findIndex(node => node.nodePath.includes(server.serverName)); + assert(index !== -1, `Failed to find server: "${server.serverName}" in OE tree`); + + const ownerUri = await azdata.connection.getUriForConnection(nodes[index].connectionId); + const now = new Date(); + + const operationId = 'testOperationId_' + now.getTime().toString(); + const sourceDB: string = 'ads_schemaCompare_sourceDB_' + now.getTime().toString(); + const targetDB: string = 'ads_schemaCompare_targetDB_' + now.getTime().toString(); + + try { + let dacfxService = await azdata.dataprotocol.getProvider('MSSQL', azdata.DataProviderType.DacFxServicesProvider); + assert(dacfxService, 'DacFx Service Provider is not available'); + let result1 = await dacfxService.deployDacpac(dacpac1, sourceDB, true, ownerUri, azdata.TaskExecutionMode.execute); + let result2 = await dacfxService.deployDacpac(dacpac2, targetDB, true, ownerUri, azdata.TaskExecutionMode.execute); + + assert(result1.success === true, 'Deploy source database should succeed'); + assert(result2.success === true, 'Deploy target database should succeed'); + utils.assertDatabaseCreationResult(sourceDB, ownerUri, retryCount); + utils.assertDatabaseCreationResult(targetDB, ownerUri, retryCount); + assert(schemaCompareService, 'Schema Compare Service Provider is not available'); + let source: azdata.SchemaCompareEndpointInfo = { + endpointType: azdata.SchemaCompareEndpointType.Database, + packageFilePath: '', + serverDisplayName: '', + serverName: server.serverName, + databaseName: sourceDB, + ownerUri: ownerUri, + }; + let target: azdata.SchemaCompareEndpointInfo = { + endpointType: azdata.SchemaCompareEndpointType.Database, + packageFilePath: '', + serverDisplayName: '', + serverName: server.serverName, + databaseName: targetDB, + ownerUri: ownerUri, + }; + + let schemaCompareResult = await schemaCompareService.schemaCompare(operationId, source, target, azdata.TaskExecutionMode.execute, null); + this.assertSchemaCompareResult(schemaCompareResult, operationId); + + let status = await schemaCompareService.schemaCompareGenerateScript(schemaCompareResult.operationId, server.serverName, targetDB, azdata.TaskExecutionMode.script); + + // TODO : add wait for tasks to complete + // script generation might take too long and the 'success' status does not mean that script is created. + await this.assertScriptGenerationResult(status, target.serverName, target.databaseName); + } + finally { + await utils.deleteDB(sourceDB, ownerUri); + await utils.deleteDB(targetDB, ownerUri); + } + } + + @stressify({ dop: SchemaCompareTester.ParallelCount }) + async SchemaCompareDacpacToDatabase(): Promise { + let server = await getStandaloneServer(); + await utils.connectToServer(server, SERVER_CONNECTION_TIMEOUT); + + let nodes = await azdata.objectexplorer.getActiveConnectionNodes(); + assert(nodes.length > 0, `Expecting at least one active connection, actual: ${nodes.length}`); + + let index = nodes.findIndex(node => node.nodePath.includes(server.serverName)); + assert(index !== -1, `Failed to find server: "${server.serverName}" in OE tree`); + + const ownerUri = await azdata.connection.getUriForConnection(nodes[index].connectionId); + const now = new Date(); + const operationId = 'testOperationId_' + now.getTime().toString(); + const targetDB: string = 'ads_schemaCompare_targetDB_' + now.getTime().toString(); + + try { + let dacfxService = await azdata.dataprotocol.getProvider('MSSQL', azdata.DataProviderType.DacFxServicesProvider); + assert(dacfxService, 'DacFx Service Provider is not available'); + let result = await dacfxService.deployDacpac(path.join(__dirname, 'testData/Database2.dacpac'), targetDB, true, ownerUri, azdata.TaskExecutionMode.execute); + + assert(result.success === true, 'Deploy database 2 (target) should succeed'); + let source: azdata.SchemaCompareEndpointInfo = { endpointType: azdata.SchemaCompareEndpointType.Dacpac, packageFilePath: dacpac1, serverDisplayName: '', serverName: '', databaseName: '', - ownerUri: '', + ownerUri: ownerUri, }; let target: azdata.SchemaCompareEndpointInfo = { - endpointType: azdata.SchemaCompareEndpointType.Dacpac, - packageFilePath: dacpac2, + endpointType: azdata.SchemaCompareEndpointType.Database, + packageFilePath: '', serverDisplayName: '', - serverName: '', - databaseName: '', - ownerUri: '', + serverName: server.serverName, + databaseName: targetDB, + ownerUri: ownerUri, }; - let schemaCompareResult = await schemaCompareService.schemaCompare(source, target, azdata.TaskExecutionMode.execute, null); - assertSchemaCompareResult(schemaCompareResult); - }); + assert(schemaCompareService, 'Schema Compare Service Provider is not available'); - test('Schema compare database to database comparison and script generation', async function () { + let schemaCompareResult = await schemaCompareService.schemaCompare(operationId, source, target, azdata.TaskExecutionMode.execute, null); + this.assertSchemaCompareResult(schemaCompareResult, operationId); - let server = await getStandaloneServer(); - await utils.connectToServer(server, SERVER_CONNECTION_TIMEOUT); + let status = await schemaCompareService.schemaCompareGenerateScript(schemaCompareResult.operationId, server.serverName, targetDB, azdata.TaskExecutionMode.script); + await this.assertScriptGenerationResult(status, target.serverName, target.databaseName); + } + finally { + await utils.deleteDB(targetDB, ownerUri); + } + } - let nodes = await azdata.objectexplorer.getActiveConnectionNodes(); - assert(nodes.length > 0, `Expecting at least one active connection, actual: ${nodes.length}`); + private assertSchemaCompareResult(schemaCompareResult: azdata.SchemaCompareResult, operationId : string): void { + assert(schemaCompareResult.areEqual === false, `Expected: the schemas are not to be equal Actual: Equal`); + assert(schemaCompareResult.errorMessage === null, `Expected: there should be no error. Actual Error message: "${schemaCompareResult.errorMessage}"`); + assert(schemaCompareResult.success === true, `Expected: success in schema compare, Actual: Failure`); + assert(schemaCompareResult.differences.length === 4, `Expected: 4 differences. Actual differences: "${schemaCompareResult.differences.length}"`); + assert(schemaCompareResult.operationId === operationId, `Operation Id Expected to be same as passed. Expected : ${operationId}, Actual ${schemaCompareResult.operationId}`) + } - let index = nodes.findIndex(node => node.nodePath.includes(server.serverName)); - assert(index !== -1, `Failed to find server: "${server.serverName}" in OE tree`); - - let ownerUri = await azdata.connection.getUriForConnection(nodes[index].connectionId); - let now = new Date(); - - let sourceDB: string = 'ads_schemaCompare_sourceDB_' + now.getTime().toString(); - let targetDB: string = 'ads_schemaCompare_targetDB_' + now.getTime().toString(); - - try { - let dacfxService = await azdata.dataprotocol.getProvider('MSSQL', azdata.DataProviderType.DacFxServicesProvider); - assert(dacfxService, 'DacFx Service Provider is not available'); - let result1 = await dacfxService.deployDacpac(dacpac1, sourceDB, true, ownerUri, azdata.TaskExecutionMode.execute); - let result2 = await dacfxService.deployDacpac(dacpac2, targetDB, true, ownerUri, azdata.TaskExecutionMode.execute); - - assert(result1.success === true, 'Deploy source database should succeed'); - assert(result2.success === true, 'Deploy target database should succeed'); - - assert(schemaCompareService, 'Schema Compare Service Provider is not available'); - - let source: azdata.SchemaCompareEndpointInfo = { - endpointType: azdata.SchemaCompareEndpointType.Database, - packageFilePath: '', - serverDisplayName: '', - serverName: server.serverName, - databaseName: sourceDB, - ownerUri: ownerUri, - }; - let target: azdata.SchemaCompareEndpointInfo = { - endpointType: azdata.SchemaCompareEndpointType.Database, - packageFilePath: '', - serverDisplayName: '', - serverName: server.serverName, - databaseName: targetDB, - ownerUri: ownerUri, - }; - - let schemaCompareResult = await schemaCompareService.schemaCompare(source, target, azdata.TaskExecutionMode.execute, null); - assertSchemaCompareResult(schemaCompareResult); - - let status = await schemaCompareService.schemaCompareGenerateScript(schemaCompareResult.operationId, server.serverName, dummyDBName, azdata.TaskExecutionMode.script); - - // TODO : add wait for tasks to complete - // script generation might take too long and the 'success' status does not mean that script is created. - await assertScriptGenerationResult(status); - } - finally { - await utils.deleteDB(sourceDB, ownerUri); - await utils.deleteDB(targetDB, ownerUri); + private async assertScriptGenerationResult(resultstatus: azdata.ResultStatus, server: string, database: string): Promise { + // TODO add more validation + assert(resultstatus.success === true, `Expected: success true Actual: "${resultstatus.success}" Error Message: "${resultstatus.errorMessage}`); + const taskService = await azdata.dataprotocol.getProvider('MSSQL', azdata.DataProviderType.TaskServicesProvider); + const tasks = await taskService.getAllTasks({ listActiveTasksOnly: true }); + let foundTask: azdata.TaskInfo; + tasks.tasks.forEach(t => { + if (t.serverName === server && t.databaseName === database && t.taskExecutionMode === azdata.TaskExecutionMode.script) { + foundTask = t; } }); - - test('Schema compare dacpac to database comparison and script generation', async function () { - let server = await getStandaloneServer(); - await utils.connectToServer(server, SERVER_CONNECTION_TIMEOUT); - - let nodes = await azdata.objectexplorer.getActiveConnectionNodes(); - assert(nodes.length > 0, `Expecting at least one active connection, actual: ${nodes.length}`); - - let index = nodes.findIndex(node => node.nodePath.includes(server.serverName)); - assert(index !== -1, `Failed to find server: "${server.serverName}" in OE tree`); - - let ownerUri = await azdata.connection.getUriForConnection(nodes[index].connectionId); - let now = new Date(); - let targetDB: string = 'ads_schemaCompare_targetDB_' + now.getTime().toString(); - - try { - let dacfxService = await azdata.dataprotocol.getProvider('MSSQL', azdata.DataProviderType.DacFxServicesProvider); - assert(dacfxService, 'DacFx Service Provider is not available'); - let result = await dacfxService.deployDacpac(path.join(__dirname, 'testData/Database2.dacpac'), targetDB, true, ownerUri, azdata.TaskExecutionMode.execute); - - assert(result.success === true, 'Deploy database 2 (target) should succeed'); - - let source: azdata.SchemaCompareEndpointInfo = { - endpointType: azdata.SchemaCompareEndpointType.Dacpac, - packageFilePath: dacpac1, - serverDisplayName: '', - serverName: '', - databaseName: '', - ownerUri: ownerUri, - }; - let target: azdata.SchemaCompareEndpointInfo = { - endpointType: azdata.SchemaCompareEndpointType.Database, - packageFilePath: '', - serverDisplayName: '', - serverName: server.serverName, - databaseName: targetDB, - ownerUri: ownerUri, - }; - - assert(schemaCompareService, 'Schema Compare Service Provider is not available'); - let schemaCompareResult = await schemaCompareService.schemaCompare(source, target, azdata.TaskExecutionMode.execute, null); - assertSchemaCompareResult(schemaCompareResult); - - let status = await schemaCompareService.schemaCompareGenerateScript(schemaCompareResult.operationId, server.serverName, dummyDBName, azdata.TaskExecutionMode.script); - await assertScriptGenerationResult(status); - } - finally { - await utils.deleteDB(targetDB, ownerUri); - } - }); - }); -} - -export function assertSchemaCompareResult(schemaCompareResult: azdata.SchemaCompareResult): void { - assert(schemaCompareResult.areEqual === false, `Expected: the schemas are not to be equal Actual: Equal`); - assert(schemaCompareResult.errorMessage === null, `Expected: there should be no error. Actual Error message: "${schemaCompareResult.errorMessage}"`); - assert(schemaCompareResult.success === true, `Expected: success in schema compare, Actual: Failure`); - assert(schemaCompareResult.differences.length === 4, `Expected: 4 differences. Actual differences: "${schemaCompareResult.differences.length}"`); -} - -export async function assertScriptGenerationResult(resultstatus: azdata.ResultStatus): Promise { - // TODO add more validation - assert(resultstatus.success === true, `Expected: success true Actual: "${resultstatus.success}" Error Message: "${resultstatus.errorMessage}`); -} + assert(foundTask, 'Could not find Script task'); + assert(foundTask.isCancelable, 'The task should be cancellable'); + } +} \ No newline at end of file diff --git a/extensions/mssql/src/config.json b/extensions/mssql/src/config.json index ecbd3a09f4..93d2d14b66 100644 --- a/extensions/mssql/src/config.json +++ b/extensions/mssql/src/config.json @@ -1,6 +1,6 @@ { "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", - "version": "1.5.0-alpha.100", + "version": "1.5.0-alpha.103", "downloadFileNames": { "Windows_86": "win-x86-netcoreapp2.2.zip", "Windows_64": "win-x64-netcoreapp2.2.zip", diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index 8ff3abc845..4d17f10c7e 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -435,6 +435,7 @@ export namespace RemoveServerGroupRequest { // ------------------------------- ----------------------------- export interface SchemaCompareParams { + operationId: string; sourceEndpointInfo: azdata.SchemaCompareEndpointInfo; targetEndpointInfo: azdata.SchemaCompareEndpointInfo; taskExecutionMode: TaskExecutionMode; @@ -466,6 +467,10 @@ export interface SchemaCompareNodeParams { taskExecutionMode: TaskExecutionMode; } +export interface SchemaCompareCancelParams { + operationId: string; +} + export namespace SchemaCompareRequest { export const type = new RequestType('schemaCompare/compare'); } @@ -486,4 +491,8 @@ export namespace SchemaCompareIncludeExcludeNodeRequest { export const type = new RequestType('schemaCompare/includeExcludeNode'); } +export namespace SchemaCompareCancellationRequest { + export const type = new RequestType('schemaCompare/cancel'); +} + // ------------------------------- ----------------------------- diff --git a/extensions/mssql/src/features.ts b/extensions/mssql/src/features.ts index b584fa3a49..4cc7912949 100644 --- a/extensions/mssql/src/features.ts +++ b/extensions/mssql/src/features.ts @@ -167,8 +167,8 @@ export class SchemaCompareServicesFeature extends SqlOpsFeature { protected registerProvider(options: undefined): Disposable { const client = this._client; - let schemaCompare = (sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode, deploymentOptions: azdata.DeploymentOptions): Thenable => { - let params: contracts.SchemaCompareParams = { sourceEndpointInfo: sourceEndpointInfo, targetEndpointInfo: targetEndpointInfo, taskExecutionMode: taskExecutionMode, deploymentOptions: deploymentOptions }; + let schemaCompare = (operationId: string, sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode, deploymentOptions: azdata.DeploymentOptions): Thenable => { + let params: contracts.SchemaCompareParams = { operationId: operationId, sourceEndpointInfo: sourceEndpointInfo, targetEndpointInfo: targetEndpointInfo, taskExecutionMode: taskExecutionMode, deploymentOptions: deploymentOptions }; return client.sendRequest(contracts.SchemaCompareRequest.type, params).then( r => { return r; @@ -232,13 +232,27 @@ export class SchemaCompareServicesFeature extends SqlOpsFeature { ); }; + let schemaCompareCancel = (operationId: string): Thenable => { + let params: contracts.SchemaCompareCancelParams = { operationId: operationId }; + return client.sendRequest(contracts.SchemaCompareCancellationRequest.type, params).then( + r => { + return r; + }, + e => { + client.logFailedRequest(contracts.SchemaCompareCancellationRequest.type, e); + return Promise.resolve(undefined); + } + ); + }; + return azdata.dataprotocol.registerSchemaCompareServicesProvider({ providerId: client.providerId, schemaCompare, schemaCompareGenerateScript, schemaComparePublishChanges, schemaCompareGetDefaultOptions, - schemaCompareIncludeExcludeNode + schemaCompareIncludeExcludeNode, + schemaCompareCancel }); } } diff --git a/extensions/schema-compare/src/media/stop-inverse.svg b/extensions/schema-compare/src/media/stop-inverse.svg new file mode 100644 index 0000000000..f24192be7c --- /dev/null +++ b/extensions/schema-compare/src/media/stop-inverse.svg @@ -0,0 +1,10 @@ + + + + +SchemaCompare + + diff --git a/extensions/schema-compare/src/media/stop.svg b/extensions/schema-compare/src/media/stop.svg new file mode 100644 index 0000000000..e4c8a390a2 --- /dev/null +++ b/extensions/schema-compare/src/media/stop.svg @@ -0,0 +1,10 @@ + + + + +SchemaCompare + + diff --git a/extensions/schema-compare/src/schemaCompareResult.ts b/extensions/schema-compare/src/schemaCompareResult.ts index c59fa1d342..32899b0b6f 100644 --- a/extensions/schema-compare/src/schemaCompareResult.ts +++ b/extensions/schema-compare/src/schemaCompareResult.ts @@ -12,6 +12,7 @@ import { SchemaCompareOptionsDialog } from './dialogs/schemaCompareOptionsDialog import { Telemetry } from './telemetry'; import { getTelemetryErrorType, getEndpointName } from './utils'; import { SchemaCompareDialog } from './dialogs/schemaCompareDialog'; +import { isNullOrUndefined } from 'util'; const localize = nls.loadMessageBundle(); const diffEditorTitle = localize('schemaCompare.CompareDetailsTitle', 'Compare Details'); const applyConfirmation = localize('schemaCompare.ApplyConfirmation', 'Are you sure you want to update the target?'); @@ -38,12 +39,14 @@ export class SchemaCompareResult { private sourceTargetFlexLayout: azdata.FlexContainer; private switchButton: azdata.ButtonComponent; private compareButton: azdata.ButtonComponent; + private cancelCompareButton: azdata.ButtonComponent; private optionsButton: azdata.ButtonComponent; private generateScriptButton: azdata.ButtonComponent; private applyButton: azdata.ButtonComponent; private selectSourceButton: azdata.ButtonComponent; private selectTargetButton: azdata.ButtonComponent; private SchemaCompareActionMap: Map; + private operationId: string; private comparisonResult: azdata.SchemaCompareResult; private sourceNameComponent: azdata.TableComponent; private targetNameComponent: azdata.TableComponent; @@ -112,6 +115,7 @@ export class SchemaCompareResult { this.createSwitchButton(view); this.createCompareButton(view); + this.createCancelButton(view); this.createGenerateScriptButton(view); this.createApplyButton(view); this.createOptionsButton(view); @@ -121,6 +125,8 @@ export class SchemaCompareResult { let toolBar = view.modelBuilder.toolbarContainer(); toolBar.addToolbarItems([{ component: this.compareButton + }, { + component: this.cancelCompareButton }, { component: this.generateScriptButton }, { @@ -247,7 +253,11 @@ export class SchemaCompareResult { } Telemetry.sendTelemetryEvent('SchemaComparisonStarted'); const service = await SchemaCompareResult.getService('MSSQL'); - this.comparisonResult = await service.schemaCompare(this.sourceEndpointInfo, this.targetEndpointInfo, azdata.TaskExecutionMode.execute, this.deploymentOptions); + if (!this.operationId) { + // create once per page + this.operationId = generateGuid(); + } + this.comparisonResult = await service.schemaCompare(this.operationId, this.sourceEndpointInfo, this.targetEndpointInfo, azdata.TaskExecutionMode.execute, this.deploymentOptions); if (!this.comparisonResult || !this.comparisonResult.success) { Telemetry.sendTelemetryEventForError('SchemaComparisonFailed', { 'errorType': getTelemetryErrorType(this.comparisonResult.errorMessage), @@ -314,6 +324,7 @@ export class SchemaCompareResult { this.switchButton.enabled = true; this.compareButton.enabled = true; this.optionsButton.enabled = true; + this.cancelCompareButton.enabled = false; if (this.comparisonResult.differences.length > 0) { this.flexModel.addItem(this.splitView, { CSSStyles: { 'overflow': 'hidden' } }); @@ -372,7 +383,7 @@ export class SchemaCompareResult { private saveExcludeState(rowState: azdata.ICheckboxCellActionEventArgs) { if (rowState) { let diff = this.comparisonResult.differences[rowState.row]; - let key = diff.sourceValue ? diff.sourceValue : diff.targetValue; + let key = (diff.sourceValue && diff.sourceValue.length > 0) ? this.createName(diff.sourceValue) : this.createName(diff.targetValue); if (key) { if (!this.sourceTargetSwitched) { this.originalSourceExcludes.delete(key); @@ -403,7 +414,7 @@ export class SchemaCompareResult { } private shouldDiffBeIncluded(diff: azdata.DiffEntry): boolean { - let key = diff.sourceValue ? diff.sourceValue : diff.targetValue; + let key = (diff.sourceValue && diff.sourceValue.length > 0) ? this.createName(diff.sourceValue) : this.createName(diff.targetValue); if (key) { if (this.sourceTargetSwitched === true && this.originalTargetExcludes.has(key)) { this.originalTargetExcludes[key] = diff; @@ -423,9 +434,9 @@ export class SchemaCompareResult { if (differences) { differences.forEach(difference => { if (difference.differenceType === azdata.SchemaDifferenceType.Object) { - if (difference.sourceValue !== null || difference.targetValue !== null) { + if ((difference.sourceValue !== null && difference.sourceValue.length > 0) || (difference.targetValue !== null && difference.targetValue.length > 0)) { let state: boolean = this.shouldDiffBeIncluded(difference); - data.push([difference.name, difference.sourceValue, state, this.SchemaCompareActionMap[difference.updateAction], difference.targetValue]); + data.push([difference.name, this.createName(difference.sourceValue), state, this.SchemaCompareActionMap[difference.updateAction], this.createName(difference.targetValue)]); } } }); @@ -434,6 +445,13 @@ export class SchemaCompareResult { return data; } + private createName(nameParts: string[]): string { + if (isNullOrUndefined(nameParts) || nameParts.length === 0) { + return ''; + } + return nameParts.join('.'); + } + private getFormattedScript(diffEntry: azdata.DiffEntry, getSourceScript: boolean): string { // if there is no entry, the script has to be \n because an empty string shows up as a difference but \n doesn't if ((getSourceScript && diffEntry.sourceScript === null) @@ -497,6 +515,54 @@ export class SchemaCompareResult { }); } + private createCancelButton(view: azdata.ModelView): void { + this.cancelCompareButton = view.modelBuilder.button().withProperties({ + label: localize('schemaCompare.cancelCompareButton', 'Stop'), + iconPath: { + light: path.join(__dirname, 'media', 'stop.svg'), + dark: path.join(__dirname, 'media', 'stop-inverse.svg') + }, + title: localize('schemaCompare.cancelCompareButtonTitle', 'Stop') + }).component(); + + this.cancelCompareButton.onDidClick(async (click) => { + await this.cancelCompare(); + }); + } + + private async cancelCompare() { + + Telemetry.sendTelemetryEvent('SchemaCompareCancelStarted', { + 'startTime:': Date.now().toString(), + 'operationId': this.operationId + }); + + // clean the pane + this.flexModel.removeItem(this.loader); + this.flexModel.removeItem(this.waitText); + this.flexModel.addItem(this.startText, { CSSStyles: { 'margin': 'auto' } }); + this.resetButtons(true); + + // cancel compare + if (this.operationId) { + const service = await SchemaCompareResult.getService('MSSQL'); + const result = await service.schemaCompareCancel(this.operationId); + + if (!result || !result.success) { + Telemetry.sendTelemetryEvent('SchemaCompareCancelFailed', { + 'errorType': getTelemetryErrorType(result.errorMessage), + 'operationId': this.operationId + }); + vscode.window.showErrorMessage( + localize('schemaCompare.cancelErrorMessage', "Cancel schema compare failed: '{0}'", (result && result.errorMessage) ? result.errorMessage : 'Unknown')); + } + Telemetry.sendTelemetryEvent('SchemaCompareCancelEnded', { + 'endTime:': Date.now().toString(), + 'operationId': this.operationId + }); + } + } + private createGenerateScriptButton(view: azdata.ModelView): void { this.generateScriptButton = view.modelBuilder.button().withProperties({ label: localize('schemaCompare.generateScriptButton', 'Generate script'), @@ -606,11 +672,13 @@ export class SchemaCompareResult { this.compareButton.enabled = true; this.optionsButton.enabled = true; this.switchButton.enabled = true; + this.cancelCompareButton.enabled = false; } else { this.compareButton.enabled = false; this.optionsButton.enabled = false; this.switchButton.enabled = false; + this.cancelCompareButton.enabled = true; } this.generateScriptButton.enabled = false; this.applyButton.enabled = false; @@ -713,4 +781,30 @@ export class SchemaCompareResult { let result = await service.schemaCompareGetDefaultOptions(); this.deploymentOptions = result.defaultDeploymentOptions; } +} + +// Borrowed as is from other extensions +// TODO : figure out if any inbuilt alternative is available +export function generateGuid(): string { + let hexValues: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']; + // c.f. rfc4122 (UUID version 4 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx) + let oct: string = ''; + let tmp: number; + /* tslint:disable:no-bitwise */ + for (let a: number = 0; a < 4; a++) { + tmp = (4294967296 * Math.random()) | 0; + oct += hexValues[tmp & 0xF] + + hexValues[tmp >> 4 & 0xF] + + hexValues[tmp >> 8 & 0xF] + + hexValues[tmp >> 12 & 0xF] + + hexValues[tmp >> 16 & 0xF] + + hexValues[tmp >> 20 & 0xF] + + hexValues[tmp >> 24 & 0xF] + + hexValues[tmp >> 28 & 0xF]; + } + + // 'Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively' + let clockSequenceHi: string = hexValues[8 + (Math.random() * 4) | 0]; + return oct.substr(0, 8) + '-' + oct.substr(9, 4) + '-4' + oct.substr(13, 3) + '-' + clockSequenceHi + oct.substr(16, 3) + '-' + oct.substr(19, 12); + /* tslint:enable:no-bitwise */ } \ No newline at end of file diff --git a/extensions/schema-compare/src/test/schemaCompare.test.ts b/extensions/schema-compare/src/test/schemaCompare.test.ts index 41625d92cf..59b43004f5 100644 --- a/extensions/schema-compare/src/test/schemaCompare.test.ts +++ b/extensions/schema-compare/src/test/schemaCompare.test.ts @@ -69,7 +69,7 @@ describe('SchemaCompareResult.start', function (): void { let result = new SchemaCompareResult(); await result.start(null); - let promise = new Promise(resolve => setTimeout(resolve, 3000)); // to ensure comparision result view is initialized + let promise = new Promise(resolve => setTimeout(resolve, 5000)); // to ensure comparison result view is initialized await promise; should(result.getComparisonResult() === undefined); diff --git a/extensions/schema-compare/src/test/testSchemaCompareService.ts b/extensions/schema-compare/src/test/testSchemaCompareService.ts index 28b1a88182..6769ed9b3c 100644 --- a/extensions/schema-compare/src/test/testSchemaCompareService.ts +++ b/extensions/schema-compare/src/test/testSchemaCompareService.ts @@ -27,7 +27,7 @@ export class SchemaCompareTestService implements azdata.SchemaCompareServicesPro throw new Error('Method not implemented.'); } - schemaCompare(sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode): Thenable { + schemaCompare(operationId: string, sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode): Thenable { let result: azdata.SchemaCompareResult = { operationId: this.testOperationId, areEqual: true, @@ -39,7 +39,11 @@ export class SchemaCompareTestService implements azdata.SchemaCompareServicesPro return Promise.resolve(result); } - schemaCompareGenerateScript(operationId: string, targetServerName: string, targetDatabaseName: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable { + schemaCompareGenerateScript(operationId: string, targetServerName: string, targetDatabaseName: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable { + return undefined; + } + + schemaCompareCancel(operationId: string): Thenable { return undefined; } diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 93be4f2d54..930592d515 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -1740,12 +1740,18 @@ declare module 'azdata' { differences: DiffEntry[]; } + export interface SchemaCompareCompletionResult extends ResultStatus { + operationId: string; + areEqual: boolean; + differences: DiffEntry[]; + } + export interface DiffEntry { updateAction: SchemaUpdateAction; differenceType: SchemaDifferenceType; name: string; - sourceValue: string; - targetValue: string; + sourceValue: string[]; + targetValue: string[]; parent: DiffEntry; children: DiffEntry[]; sourceScript: string; @@ -1930,11 +1936,12 @@ declare module 'azdata' { } export interface SchemaCompareServicesProvider extends DataProvider { - schemaCompare(sourceEndpointInfo: SchemaCompareEndpointInfo, targetEndpointInfo: SchemaCompareEndpointInfo, taskExecutionMode: TaskExecutionMode, deploymentOptions: DeploymentOptions): Thenable; + schemaCompare(operationId: string, sourceEndpointInfo: SchemaCompareEndpointInfo, targetEndpointInfo: SchemaCompareEndpointInfo, taskExecutionMode: TaskExecutionMode, deploymentOptions: DeploymentOptions): Thenable; schemaCompareGenerateScript(operationId: string, targetServerName: string, targetDatabaseName: string, taskExecutionMode: TaskExecutionMode): Thenable; schemaComparePublishChanges(operationId: string, targetServerName: string, targetDatabaseName: string, taskExecutionMode: TaskExecutionMode): Thenable; schemaCompareGetDefaultOptions(): Thenable; schemaCompareIncludeExcludeNode(operationId: string, diffEntry: DiffEntry, IncludeRequest: boolean, taskExecutionMode: TaskExecutionMode): Thenable; + schemaCompareCancel(operationId: string): Thenable; } // Security service interfaces ------------------------------------------------------------------------ diff --git a/src/sql/platform/schemaCompare/common/schemaCompareService.ts b/src/sql/platform/schemaCompare/common/schemaCompareService.ts index 2db0ebd5b8..9c98ae05bc 100644 --- a/src/sql/platform/schemaCompare/common/schemaCompareService.ts +++ b/src/sql/platform/schemaCompare/common/schemaCompareService.ts @@ -15,11 +15,12 @@ export interface ISchemaCompareService { _serviceBrand: any; registerProvider(providerId: string, provider: azdata.SchemaCompareServicesProvider): void; - schemaCompare(sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode, deploymentOptions: azdata.DeploymentOptions): void; + schemaCompare(operationId: string, sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode, deploymentOptions: azdata.DeploymentOptions): void; schemaCompareGenerateScript(operationId: string, targetServerName: string, targetDatabaseName: string, taskExecutionMode: azdata.TaskExecutionMode): void; schemaComparePublishChanges(operationId: string, targetServerName: string, targetDatabaseName: string, taskExecutionMode: azdata.TaskExecutionMode): void; schemaCompareGetDefaultOptions(): void; schemaCompareIncludeExcludeNode(operationId: string, diffEntry: azdata.DiffEntry, includeRequest: boolean, taskExecutionMode: azdata.TaskExecutionMode): void; + schemaCompareCancel(operationId: string): void; } export class SchemaCompareService implements ISchemaCompareService { @@ -32,9 +33,9 @@ export class SchemaCompareService implements ISchemaCompareService { this._providers[providerId] = provider; } - schemaCompare(sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode, deploymentOptions: azdata.DeploymentOptions): Thenable { + schemaCompare(operationId: string, sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode, deploymentOptions: azdata.DeploymentOptions): Thenable { return this._runAction(sourceEndpointInfo.ownerUri, (runner) => { - return runner.schemaCompare(sourceEndpointInfo, targetEndpointInfo, taskExecutionMode, deploymentOptions); + return runner.schemaCompare(operationId, sourceEndpointInfo, targetEndpointInfo, taskExecutionMode, deploymentOptions); }); } @@ -62,6 +63,12 @@ export class SchemaCompareService implements ISchemaCompareService { }); } + schemaCompareCancel(operationId: string): Thenable { + return this._runAction('', (runner) => { + return runner.schemaCompareCancel(operationId); + }); + } + private _runAction(uri: string, action: (handler: azdata.SchemaCompareServicesProvider) => Thenable): Thenable { let providerId: string = this._connectionService.getProviderIdFromUri(uri); diff --git a/src/sql/workbench/api/node/mainThreadDataProtocol.ts b/src/sql/workbench/api/node/mainThreadDataProtocol.ts index d9abc1d4ed..c75e18db4d 100644 --- a/src/sql/workbench/api/node/mainThreadDataProtocol.ts +++ b/src/sql/workbench/api/node/mainThreadDataProtocol.ts @@ -470,8 +470,8 @@ export class MainThreadDataProtocol implements MainThreadDataProtocolShape { public $registerSchemaCompareServicesProvider(providerId: string, handle: number): Promise { const self = this; this._schemaCompareService.registerProvider(providerId, { - schemaCompare(sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode, schemaComapareOptions: azdata.DeploymentOptions): Thenable { - return self._proxy.$schemaCompare(handle, sourceEndpointInfo, targetEndpointInfo, taskExecutionMode, schemaComapareOptions); + schemaCompare(operationId: string, sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode, schemaComapareOptions: azdata.DeploymentOptions): Thenable { + return self._proxy.$schemaCompare(handle, operationId, sourceEndpointInfo, targetEndpointInfo, taskExecutionMode, schemaComapareOptions); }, schemaCompareGenerateScript(operationId: string, targetServerName: string, targetDatabaseName: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable { return self._proxy.$schemaCompareGenerateScript(handle, operationId, targetServerName, targetDatabaseName, taskExecutionMode); @@ -484,6 +484,9 @@ export class MainThreadDataProtocol implements MainThreadDataProtocolShape { }, schemaCompareIncludeExcludeNode(operationId: string, diffEntry: azdata.DiffEntry, includeRequest: boolean, taskExecutionMode: azdata.TaskExecutionMode): Thenable { return self._proxy.$schemaCompareIncludeExcludeNode(handle, operationId, diffEntry, includeRequest, taskExecutionMode); + }, + schemaCompareCancel(operationId: string): Thenable { + return self._proxy.$schemaCompareCancel(handle, operationId); } }); diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index fff911764d..8cecc7ab07 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -465,7 +465,7 @@ export abstract class ExtHostDataProtocolShape { /** * Schema compare */ - $schemaCompare(handle: number, sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode, schemaComapareOptions: azdata.DeploymentOptions): Thenable { throw ni(); } + $schemaCompare(handle: number, operationId: string, sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode, schemaComapareOptions: azdata.DeploymentOptions): Thenable { throw ni(); } /** * Schema compare generate script @@ -487,6 +487,12 @@ export abstract class ExtHostDataProtocolShape { * Schema compare Include node */ $schemaCompareIncludeExcludeNode(handle: number, operationId: string, diffEntry: azdata.DiffEntry, includeRequest: boolean, taskExecutionMode: azdata.TaskExecutionMode): Thenable { throw ni(); } + + /** + * Schema compare cancel + */ + $schemaCompareCancel(handle: number, operationId: string): Thenable { throw ni(); } + } /**