diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index 4d17f10c7e..ca33e60966 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -467,6 +467,16 @@ export interface SchemaCompareNodeParams { taskExecutionMode: TaskExecutionMode; } +export interface SchemaCompareSaveScmpParams { + sourceEndpointInfo: azdata.SchemaCompareEndpointInfo; + targetEndpointInfo: azdata.SchemaCompareEndpointInfo; + taskExecutionMode: TaskExecutionMode; + deploymentOptions: azdata.DeploymentOptions; + scmpFilePath: string; + excludedSourceObjects: azdata.SchemaCompareObjectId[]; + excludedTargetObjects: azdata.SchemaCompareObjectId[]; +} + export interface SchemaCompareCancelParams { operationId: string; } @@ -491,6 +501,10 @@ export namespace SchemaCompareIncludeExcludeNodeRequest { export const type = new RequestType('schemaCompare/includeExcludeNode'); } +export namespace SchemaCompareSaveScmpRequest { + export const type = new RequestType('schemaCompare/saveScmp'); +} + 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 4cc7912949..7f39698c9d 100644 --- a/extensions/mssql/src/features.ts +++ b/extensions/mssql/src/features.ts @@ -147,7 +147,8 @@ export class SchemaCompareServicesFeature extends SqlOpsFeature { contracts.SchemaCompareRequest.type, contracts.SchemaCompareGenerateScriptRequest.type, contracts.SchemaCompareGetDefaultOptionsRequest.type, - contracts.SchemaCompareIncludeExcludeNodeRequest.type + contracts.SchemaCompareIncludeExcludeNodeRequest.type, + contracts.SchemaCompareSaveScmpRequest.type ]; constructor(client: SqlOpsDataClient) { @@ -219,7 +220,7 @@ export class SchemaCompareServicesFeature extends SqlOpsFeature { ); }; - let schemaCompareIncludeExcludeNode = (operationId: string, diffEntry: azdata.DiffEntry, includeRequest: boolean, taskExecutionMode: azdata.TaskExecutionMode): Thenable => { + let schemaCompareIncludeExcludeNode = (operationId: string, diffEntry: azdata.DiffEntry, includeRequest: boolean, taskExecutionMode: azdata.TaskExecutionMode): Thenable => { let params: contracts.SchemaCompareNodeParams = { operationId: operationId, diffEntry, includeRequest, taskExecutionMode: taskExecutionMode }; return client.sendRequest(contracts.SchemaCompareIncludeExcludeNodeRequest.type, params).then( r => { @@ -232,6 +233,19 @@ export class SchemaCompareServicesFeature extends SqlOpsFeature { ); }; + let schemaCompareSaveScmp = (sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode, deploymentOptions: azdata.DeploymentOptions, scmpFilePath: string, excludedSourceObjects: azdata.SchemaCompareObjectId[], excludedTargetObjects: azdata.SchemaCompareObjectId[]): Thenable => { + let params: contracts.SchemaCompareSaveScmpParams = { sourceEndpointInfo: sourceEndpointInfo, targetEndpointInfo: targetEndpointInfo, taskExecutionMode: taskExecutionMode, deploymentOptions: deploymentOptions, scmpFilePath: scmpFilePath, excludedSourceObjects: excludedSourceObjects, excludedTargetObjects: excludedTargetObjects }; + return client.sendRequest(contracts.SchemaCompareSaveScmpRequest.type, params).then( + r => { + return r; + }, + e => { + client.logFailedRequest(contracts.SchemaCompareSaveScmpRequest.type, e); + return Promise.resolve(undefined); + } + ); + }; + let schemaCompareCancel = (operationId: string): Thenable => { let params: contracts.SchemaCompareCancelParams = { operationId: operationId }; return client.sendRequest(contracts.SchemaCompareCancellationRequest.type, params).then( @@ -252,6 +266,7 @@ export class SchemaCompareServicesFeature extends SqlOpsFeature { schemaComparePublishChanges, schemaCompareGetDefaultOptions, schemaCompareIncludeExcludeNode, + schemaCompareSaveScmp, schemaCompareCancel }); } diff --git a/extensions/schema-compare/src/media/save-scmp-inverse.svg b/extensions/schema-compare/src/media/save-scmp-inverse.svg new file mode 100644 index 0000000000..3725b091f7 --- /dev/null +++ b/extensions/schema-compare/src/media/save-scmp-inverse.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/schema-compare/src/media/save-scmp.svg b/extensions/schema-compare/src/media/save-scmp.svg new file mode 100644 index 0000000000..42e14dde77 --- /dev/null +++ b/extensions/schema-compare/src/media/save-scmp.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/schema-compare/src/schemaCompareResult.ts b/extensions/schema-compare/src/schemaCompareResult.ts index c756a6f9de..a5fccfcce9 100644 --- a/extensions/schema-compare/src/schemaCompareResult.ts +++ b/extensions/schema-compare/src/schemaCompareResult.ts @@ -46,6 +46,7 @@ export class SchemaCompareResult { private applyButton: azdata.ButtonComponent; private selectSourceButton: azdata.ButtonComponent; private selectTargetButton: azdata.ButtonComponent; + private saveScmpButton: azdata.ButtonComponent; private SchemaCompareActionMap: Map; private operationId: string; private comparisonResult: azdata.SchemaCompareResult; @@ -121,6 +122,7 @@ export class SchemaCompareResult { this.createApplyButton(view); this.createOptionsButton(view); this.createSourceAndTargetButtons(view); + this.createSaveScmpButton(view); this.resetButtons(false); // disable buttons because source and target aren't both selected yet let toolBar = view.modelBuilder.toolbarContainer(); @@ -136,7 +138,10 @@ export class SchemaCompareResult { component: this.optionsButton, toolbarSeparatorAfter: true }, { - component: this.switchButton + component: this.switchButton, + toolbarSeparatorAfter: true + }, { + component: this.saveScmpButton }]); let sourceLabel = view.modelBuilder.text().withProperties({ @@ -674,6 +679,7 @@ export class SchemaCompareResult { this.optionsButton.enabled = true; this.switchButton.enabled = true; this.cancelCompareButton.enabled = false; + this.saveScmpButton.enabled = true; } else { this.compareButton.enabled = false; @@ -761,6 +767,72 @@ export class SchemaCompareResult { }); } + private createSaveScmpButton(view: azdata.ModelView): void { + this.saveScmpButton = view.modelBuilder.button().withProperties({ + label: localize('schemaCompare.saveScmpButton', 'Save .scmp file'), + iconPath: { + light: path.join(__dirname, 'media', 'save-scmp.svg'), + dark: path.join(__dirname, 'media', 'save-scmp-inverse.svg') + }, + title: localize('schemaCompare.saveScmpButtonTitle', 'Save source and target, options, and excluded elements'), + enabled: false + }).component(); + + this.saveScmpButton.onDidClick(async (click) => { + const rootPath = vscode.workspace.rootPath ? vscode.workspace.rootPath : os.homedir(); + const filePath = await vscode.window.showSaveDialog( + { + defaultUri: vscode.Uri.file(rootPath), + saveLabel: localize('schemaCompare.saveFile', 'Save'), + filters: { + 'scmp Files': ['scmp'], + } + } + ); + + if (!filePath) { + return; + } + + // convert include/exclude maps to arrays of object ids + let sourceExcludes: azdata.SchemaCompareObjectId[] = this.convertExcludesToObjectIds(this.originalSourceExcludes); + let targetExcludes: azdata.SchemaCompareObjectId[] = this.convertExcludesToObjectIds(this.originalTargetExcludes); + + let startTime = Date.now(); + Telemetry.sendTelemetryEvent('SchemaCompareSaveScmp'); + const service = await SchemaCompareResult.getService(msSqlProvider); + const result = await service.schemaCompareSaveScmp(this.sourceEndpointInfo, this.targetEndpointInfo, azdata.TaskExecutionMode.execute, this.deploymentOptions, filePath.fsPath, sourceExcludes, targetExcludes); + if (!result || !result.success) { + Telemetry.sendTelemetryEvent('SchemaCompareSaveScmpFailed', { + 'errorType': getTelemetryErrorType(result.errorMessage), + 'operationId': this.comparisonResult.operationId + }); + vscode.window.showErrorMessage( + localize('schemaCompare.saveScmpErrorMessage', "Save scmp failed: '{0}'", (result && result.errorMessage) ? result.errorMessage : 'Unknown')); + } + + Telemetry.sendTelemetryEvent('SchemaCompareSaveScmpEnded', { + 'totalSaveTime:': (Date.now() - startTime).toString(), + 'operationId': this.comparisonResult.operationId + }); + }); + } + + /** + * Converts excluded diff entries into object ids which are needed to save them in an scmp + */ + private convertExcludesToObjectIds(excludedDiffEntries: Map): azdata.SchemaCompareObjectId[] { + let result = []; + excludedDiffEntries.forEach((value: azdata.DiffEntry) => { + result.push({ + nameParts: value.sourceValue ? value.sourceValue : value.targetValue, + sqlObjectType: `Microsoft.Data.Tools.Schema.Sql.SchemaModel.${value.name}` + }); + }); + + return result; + } + private setButtonStatesForNoChanges(enableButtons: boolean): void { // generate script and apply can only be enabled if the target is a database if (this.targetEndpointInfo.endpointType === azdata.SchemaCompareEndpointType.Database) { diff --git a/extensions/schema-compare/src/test/testSchemaCompareService.ts b/extensions/schema-compare/src/test/testSchemaCompareService.ts index 6769ed9b3c..bd0cc0c1d2 100644 --- a/extensions/schema-compare/src/test/testSchemaCompareService.ts +++ b/extensions/schema-compare/src/test/testSchemaCompareService.ts @@ -27,6 +27,11 @@ export class SchemaCompareTestService implements azdata.SchemaCompareServicesPro throw new Error('Method not implemented.'); } + + schemaCompareSaveScmp(sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode, deploymentOptions: azdata.DeploymentOptions, scmpFilePath: string, excludedSourceObjects: azdata.SchemaCompareObjectId[], excludedTargetObjects: azdata.SchemaCompareObjectId[]): Thenable { + throw new Error('Method not implemented.'); + } + schemaCompare(operationId: string, sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode): Thenable { let result: azdata.SchemaCompareResult = { operationId: this.testOperationId, diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 930592d515..05d7d91fa4 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -1781,6 +1781,11 @@ declare module 'azdata' { ownerUri: string; } + export interface SchemaCompareObjectId { + nameParts: string[]; + sqlObjectType: string; + } + export interface SchemaCompareOptionsResult extends ResultStatus { defaultDeploymentOptions: DeploymentOptions; } @@ -1941,6 +1946,7 @@ declare module 'azdata' { schemaComparePublishChanges(operationId: string, targetServerName: string, targetDatabaseName: string, taskExecutionMode: TaskExecutionMode): Thenable; schemaCompareGetDefaultOptions(): Thenable; schemaCompareIncludeExcludeNode(operationId: string, diffEntry: DiffEntry, IncludeRequest: boolean, taskExecutionMode: TaskExecutionMode): Thenable; + schemaCompareSaveScmp(sourceEndpointInfo: SchemaCompareEndpointInfo, targetEndpointInfo: SchemaCompareEndpointInfo, taskExecutionMode: TaskExecutionMode, deploymentOptions: DeploymentOptions, scmpFilePath: string, excludedSourceObjects: SchemaCompareObjectId[], excludedTargetObjects: SchemaCompareObjectId[]): Thenable; schemaCompareCancel(operationId: string): Thenable; } diff --git a/src/sql/platform/schemaCompare/common/schemaCompareService.ts b/src/sql/platform/schemaCompare/common/schemaCompareService.ts index 9c98ae05bc..1e0646211e 100644 --- a/src/sql/platform/schemaCompare/common/schemaCompareService.ts +++ b/src/sql/platform/schemaCompare/common/schemaCompareService.ts @@ -20,6 +20,7 @@ export interface ISchemaCompareService { 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; + schemaCompareSaveScmp(sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode, deploymentOptions: azdata.DeploymentOptions, scmpFilePath: string, excludedSourceObjects: azdata.SchemaCompareObjectId[], excludedTargetObjects: azdata.SchemaCompareObjectId[]); schemaCompareCancel(operationId: string): void; } @@ -63,6 +64,12 @@ export class SchemaCompareService implements ISchemaCompareService { }); } + schemaCompareSaveScmp(sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode, deploymentOptions: azdata.DeploymentOptions, scmpFilePath: string, excludedSourceObjects: azdata.SchemaCompareObjectId[], excludedTargetObjects: azdata.SchemaCompareObjectId[]) { + return this._runAction('', (runner) => { + return runner.schemaCompareSaveScmp(sourceEndpointInfo, targetEndpointInfo, taskExecutionMode, deploymentOptions, scmpFilePath, excludedSourceObjects, excludedTargetObjects); + }); + } + schemaCompareCancel(operationId: string): Thenable { return this._runAction('', (runner) => { return runner.schemaCompareCancel(operationId); diff --git a/src/sql/workbench/api/node/mainThreadDataProtocol.ts b/src/sql/workbench/api/node/mainThreadDataProtocol.ts index c75e18db4d..ef202d46eb 100644 --- a/src/sql/workbench/api/node/mainThreadDataProtocol.ts +++ b/src/sql/workbench/api/node/mainThreadDataProtocol.ts @@ -485,6 +485,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); }, + schemaCompareSaveScmp(sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode, deploymentOptions: azdata.DeploymentOptions, scmpFilePath: string, excludedSourceObjects: azdata.SchemaCompareObjectId[], excludedTargetObjects: azdata.SchemaCompareObjectId[]): Thenable { + return self._proxy.$schemaCompareSaveScmp(handle, sourceEndpointInfo, targetEndpointInfo, taskExecutionMode, deploymentOptions, scmpFilePath, excludedSourceObjects, excludedTargetObjects); + }, 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 8cecc7ab07..29f519fb87 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -488,6 +488,11 @@ export abstract class ExtHostDataProtocolShape { */ $schemaCompareIncludeExcludeNode(handle: number, operationId: string, diffEntry: azdata.DiffEntry, includeRequest: boolean, taskExecutionMode: azdata.TaskExecutionMode): Thenable { throw ni(); } + /** + * Schema compare save scmp + */ + $schemaCompareSaveScmp(handle: number, sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode, deploymentOptions: azdata.DeploymentOptions, scmpFilePath: string, excludedSourceObjects: azdata.SchemaCompareObjectId[], excludedTargetObjects: azdata.SchemaCompareObjectId[]): Thenable { throw ni(); } + /** * Schema compare cancel */