From d3483afaed2577be7dee24f89ed6a018f49719d8 Mon Sep 17 00:00:00 2001 From: kisantia <31145923+kisantia@users.noreply.github.com> Date: Wed, 17 Apr 2019 19:14:22 -0700 Subject: [PATCH] Schema Compare extension (#4974) * extension now working * fix diff editor title disappearing and remove border from source and target name boxes * redoing a bunch of stuff that disappeared after rebasing * add images and add to extensions.ts * moving a few changes to the right place after rebase * formatting * update toolbar svgs * addressing comments * add return types * Adding PR comments * Adding light and dark theme icons * Fixing the diff editor title for dark theme --- build/lib/extensions.js | 3 +- build/lib/extensions.ts | 3 +- extensions/mssql/src/contracts.ts | 25 +- extensions/mssql/src/features.ts | 58 +++ extensions/mssql/src/main.ts | 3 +- extensions/schema-compare/README.md | 29 ++ .../schema-compare/images/dark_icon.svg | 1 + .../schema-compare/images/light_icon.svg | 1 + .../schema-compare/images/sqlserver.png | Bin 0 -> 37585 bytes extensions/schema-compare/package.json | 52 ++ .../src/controllers/mainController.ts | 41 ++ .../src/dialogs/schemaCompareDialog.ts | 462 ++++++++++++++++++ extensions/schema-compare/src/main.ts | 24 + .../src/media/compare-inverse.svg | 10 + .../schema-compare/src/media/compare.svg | 10 + .../src/media/generate-script-inverse.svg | 3 + .../src/media/generate-script.svg | 3 + .../src/media/switch-directions-inverse.svg | 3 + .../src/media/switch-directions.svg | 3 + .../schema-compare/src/schemaCompareResult.ts | 366 ++++++++++++++ .../schema-compare/src/typings/ref.d.ts | 9 + extensions/schema-compare/tsconfig.json | 20 + extensions/schema-compare/yarn.lock | 46 ++ src/sql/azdata.proposed.d.ts | 49 ++ .../modelComponents/diffeditor.component.ts | 3 +- src/sql/parts/modelComponents/editor.css | 8 + .../parts/modelComponents/table.component.ts | 1 + src/sql/parts/modelComponents/table.css | 5 + .../common/schemaCompareService.ts | 59 +++ src/sql/sqlops.d.ts | 2 + src/sql/sqlops.proposed.d.ts | 1 + .../workbench/api/common/sqlExtHostTypes.ts | 17 + .../workbench/api/node/extHostDataProtocol.ts | 6 + .../api/node/mainThreadDataProtocol.ts | 16 + .../workbench/api/node/sqlExtHost.api.impl.ts | 11 +- .../workbench/api/node/sqlExtHost.protocol.ts | 10 + src/vs/workbench/workbench.main.ts | 2 + 37 files changed, 1359 insertions(+), 6 deletions(-) create mode 100644 extensions/schema-compare/README.md create mode 100644 extensions/schema-compare/images/dark_icon.svg create mode 100644 extensions/schema-compare/images/light_icon.svg create mode 100644 extensions/schema-compare/images/sqlserver.png create mode 100644 extensions/schema-compare/package.json create mode 100644 extensions/schema-compare/src/controllers/mainController.ts create mode 100644 extensions/schema-compare/src/dialogs/schemaCompareDialog.ts create mode 100644 extensions/schema-compare/src/main.ts create mode 100644 extensions/schema-compare/src/media/compare-inverse.svg create mode 100644 extensions/schema-compare/src/media/compare.svg create mode 100644 extensions/schema-compare/src/media/generate-script-inverse.svg create mode 100644 extensions/schema-compare/src/media/generate-script.svg create mode 100644 extensions/schema-compare/src/media/switch-directions-inverse.svg create mode 100644 extensions/schema-compare/src/media/switch-directions.svg create mode 100644 extensions/schema-compare/src/schemaCompareResult.ts create mode 100644 extensions/schema-compare/src/typings/ref.d.ts create mode 100644 extensions/schema-compare/tsconfig.json create mode 100644 extensions/schema-compare/yarn.lock create mode 100644 src/sql/platform/schemaCompare/common/schemaCompareService.ts diff --git a/build/lib/extensions.js b/build/lib/extensions.js index 2cf9bca6d2..d4ac681191 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -253,7 +253,8 @@ const sqlBuiltInExtensions = [ 'profiler', 'admin-pack', 'big-data-cluster', - 'dacpac' + 'dacpac', + 'schema-compare' ]; const builtInExtensions = require('../builtInExtensions.json'); /** diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 9cb9021270..41108af912 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -300,7 +300,8 @@ const sqlBuiltInExtensions = [ 'profiler', 'admin-pack', 'big-data-cluster', - 'dacpac' + 'dacpac', + 'schema-compare' ]; // {{SQL CARBON EDIT}} - End diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index 53146fbed3..0b5437aaa9 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -433,4 +433,27 @@ export namespace AddServerGroupRequest { export namespace RemoveServerGroupRequest { export const type = new RequestType('cms/removeCmsServerGroup'); } -// ------------------------------- ---------------------------------------- \ No newline at end of file +// ------------------------------- ---------------------------------------- + +// ------------------------------- ----------------------------- +export interface SchemaCompareParams { + sourceEndpointInfo: azdata.SchemaCompareEndpointInfo; + targetEndpointInfo: azdata.SchemaCompareEndpointInfo; + taskExecutionMode: TaskExecutionMode; +} + + export interface SchemaCompareGenerateScriptParams { + operationId: string; + targetDatabaseName: string; + scriptFilePath: string; + taskExecutionMode: TaskExecutionMode; +} + +export namespace SchemaCompareRequest { + export const type = new RequestType('schemaCompare/compare'); +} + + export namespace SchemaCompareGenerateScriptRequest { + export const type = new RequestType('schemaCompare/generateScript'); +} +// ------------------------------- ----------------------------- diff --git a/extensions/mssql/src/features.ts b/extensions/mssql/src/features.ts index 210a8820ba..4f19c416bf 100644 --- a/extensions/mssql/src/features.ts +++ b/extensions/mssql/src/features.ts @@ -145,6 +145,64 @@ export class DacFxServicesFeature extends SqlOpsFeature { } } +export class SchemaCompareServicesFeature extends SqlOpsFeature { + private static readonly messageTypes: RPCMessageType[] = [ + contracts.SchemaCompareRequest.type, + contracts.SchemaCompareGenerateScriptRequest.type + ]; + + constructor(client: SqlOpsDataClient) { + super(client, SchemaCompareServicesFeature.messageTypes); + } + + public fillClientCapabilities(capabilities: ClientCapabilities): void { + } + + public initialize(capabilities: ServerCapabilities): void { + this.register(this.messages, { + id: UUID.generateUuid(), + registerOptions: undefined + }); + } + + protected registerProvider(options: undefined): Disposable { + const client = this._client; + let self = this; + + let schemaCompare = (sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode): Thenable => { + let params: contracts.SchemaCompareParams = {sourceEndpointInfo: sourceEndpointInfo, targetEndpointInfo: targetEndpointInfo, taskExecutionMode: taskExecutionMode}; + return client.sendRequest(contracts.SchemaCompareRequest.type, params).then( + r => { + return r; + }, + e => { + client.logFailedRequest(contracts.SchemaCompareRequest.type, e); + return Promise.resolve(undefined); + } + ); + }; + + let schemaCompareGenerateScript = (operationId: string, targetDatabaseName: string, scriptFilePath: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable => { + let params: contracts.SchemaCompareGenerateScriptParams = {operationId: operationId, targetDatabaseName: targetDatabaseName, scriptFilePath: scriptFilePath, taskExecutionMode: taskExecutionMode}; + return client.sendRequest(contracts.SchemaCompareGenerateScriptRequest.type, params).then( + r => { + return r; + }, + e => { + client.logFailedRequest(contracts.SchemaCompareGenerateScriptRequest.type, e); + return Promise.resolve(undefined); + } + ); + }; + + return azdata.dataprotocol.registerSchemaCompareServicesProvider({ + providerId: client.providerId, + schemaCompare, + schemaCompareGenerateScript + }); + } +} + export class AgentServicesFeature extends SqlOpsFeature { private static readonly messagesTypes: RPCMessageType[] = [ contracts.AgentJobsRequest.type, diff --git a/extensions/mssql/src/main.ts b/extensions/mssql/src/main.ts index 3db280c18a..59446a8ae1 100644 --- a/extensions/mssql/src/main.ts +++ b/extensions/mssql/src/main.ts @@ -21,7 +21,7 @@ import { CredentialStore } from './credentialstore/credentialstore'; import { AzureResourceProvider } from './resourceProvider/resourceProvider'; import * as Utils from './utils'; import { Telemetry, LanguageClientErrorHandler } from './telemetry'; -import { TelemetryFeature, AgentServicesFeature, DacFxServicesFeature } from './features'; +import { TelemetryFeature, AgentServicesFeature, DacFxServicesFeature, SchemaCompareServicesFeature } from './features'; import { AppContext } from './appContext'; import { ApiWrapper } from './apiWrapper'; import { UploadFilesCommand, MkDirCommand, SaveFileCommand, PreviewFileCommand, CopyPathCommand, DeleteFilesCommand } from './objectExplorerNodeProvider/hdfsCommands'; @@ -77,6 +77,7 @@ export async function activate(context: vscode.ExtensionContext): Promiseimportflatfile_inverse \ No newline at end of file diff --git a/extensions/schema-compare/images/light_icon.svg b/extensions/schema-compare/images/light_icon.svg new file mode 100644 index 0000000000..221399119c --- /dev/null +++ b/extensions/schema-compare/images/light_icon.svg @@ -0,0 +1 @@ +importflatfile \ No newline at end of file diff --git a/extensions/schema-compare/images/sqlserver.png b/extensions/schema-compare/images/sqlserver.png new file mode 100644 index 0000000000000000000000000000000000000000..d884faa14a804f63aa2a9d365fc75c09c12eeeb5 GIT binary patch literal 37585 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 RTp9-alNMJHs}M2r`#&r#oSOgu literal 0 HcmV?d00001 diff --git a/extensions/schema-compare/package.json b/extensions/schema-compare/package.json new file mode 100644 index 0000000000..51ab4649e3 --- /dev/null +++ b/extensions/schema-compare/package.json @@ -0,0 +1,52 @@ +{ + "name": "schema-compare", + "displayName": "SQL Server Schema Compare", + "description": "SQL Server Schema Compare for Azure Data Studio supports comparing the schemas of databases and dacpacs.", + "version": "0.1.0", + "publisher": "Microsoft", + "preview": true, + "engines": { + "vscode": "^1.25.0", + "sqlops": "*" + }, + "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/extensions/import/Microsoft_SQL_Server_Import_Extension_and_Tools_Import_Flat_File_Preview.docx", + "icon": "images/sqlserver.png", + "aiKey": "AIF-5574968e-856d-40d2-af67-c89a14e76412", + "activationEvents": [ + "*" + ], + "main": "./out/main", + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/azuredatastudio.git" + }, + "extensionDependencies": [ + "Microsoft.mssql" + ], + "contributes": { + "commands": [ + { + "command": "schemaCompare.start", + "title": "Schema Compare", + "icon": { + "light": "./images/light_icon.svg", + "dark": "./images/dark_icon.svg" + } + } + ], + "menus": { + "objectExplorer/item/context": [ + { + "command": "schemaCompare.start", + "when": "connectionProvider == MSSQL && nodeType && nodeType == Database", + "group": "export" + } + ] + } + }, + "dependencies": { + "vscode-extension-telemetry": "0.0.18", + "vscode-nls": "^3.2.1" + }, + "devDependencies": {} +} diff --git a/extensions/schema-compare/src/controllers/mainController.ts b/extensions/schema-compare/src/controllers/mainController.ts new file mode 100644 index 0000000000..fd884eeca1 --- /dev/null +++ b/extensions/schema-compare/src/controllers/mainController.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; +import { SchemaCompareDialog } from '../dialogs/schemaCompareDialog'; + +/** + * The main controller class that initializes the extension + */ +export default class MainController implements vscode.Disposable { + protected _context: vscode.ExtensionContext; + + public constructor(context: vscode.ExtensionContext) { + this._context = context; + } + + public get extensionContext(): vscode.ExtensionContext { + return this._context; + } + + public deactivate(): void { + } + + public activate(): Promise { + this.initializeSchemaCompareDialog(); + return Promise.resolve(true); + } + + private initializeSchemaCompareDialog(): void { + azdata.tasks.registerTask('schemaCompare.start', (profile: azdata.IConnectionProfile) => new SchemaCompareDialog().openDialog(profile)); + } + + public dispose(): void { + this.deactivate(); + } +} diff --git a/extensions/schema-compare/src/dialogs/schemaCompareDialog.ts b/extensions/schema-compare/src/dialogs/schemaCompareDialog.ts new file mode 100644 index 0000000000..e1b5e040ac --- /dev/null +++ b/extensions/schema-compare/src/dialogs/schemaCompareDialog.ts @@ -0,0 +1,462 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as nls from 'vscode-nls'; +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; +import * as os from 'os'; +import { SchemaCompareResult } from '../schemaCompareResult'; + +const localize = nls.loadMessageBundle(); +const CompareButtonText: string = localize('schemaCompareDialog.Compare', 'Compare'); +const CancelButtonText: string = localize('schemaCompareDialog.Cancel', 'Cancel'); +const SourceTextBoxLabel: string = localize('schemaCompareDialog.SourceLabel', 'Source File'); +const TargetTextBoxLabel: string = localize('schemaCompareDialog.TargetLabel', 'Target File'); +const DacpacRadioButtonLabel: string = localize('schemaCompare.dacpacRadioButtonLabel', 'Data-tier Application File (.dacpac)'); +const DatabaseRadioButtonLabel: string = localize('schemaCompare.databaseButtonLabel', 'Database'); +const SourceRadioButtonsLabel: string = localize('schemaCompare.sourceButtonsLabel', 'Source Type'); +const TargetRadioButtonsLabel: string = localize('schemaCompare.targetButtonsLabel', 'Target Type'); +const NoActiveConnectionsLabel: string = localize('schemaCompare.NoActiveConnectionsText', 'No active connections'); +const SchemaCompareLabel: string = localize('schemaCompare.dialogTitle', 'Schema Compare'); + +export class SchemaCompareDialog { + public dialog: azdata.window.Dialog; + private schemaCompareTab: azdata.window.DialogTab; + private sourceDacpacComponent: azdata.FormComponent; + private sourceTextBox: azdata.InputBoxComponent; + private sourceFileButton: azdata.ButtonComponent; + private sourceServerComponent: azdata.FormComponent; + private sourceServerDropdown: azdata.DropDownComponent; + private sourceDatabaseComponent: azdata.FormComponent; + private sourceDatabaseDropdown: azdata.DropDownComponent; + private sourceNoActiveConnectionsText: azdata.FormComponent; + private targetDacpacComponent: azdata.FormComponent; + private targetTextBox: azdata.InputBoxComponent; + private targetFileButton: azdata.ButtonComponent; + private targetServerComponent: azdata.FormComponent; + private targetServerDropdown: azdata.DropDownComponent; + private targetDatabaseComponent: azdata.FormComponent; + private targetDatabaseDropdown: azdata.DropDownComponent; + private targetNoActiveConnectionsText: azdata.FormComponent; + private formBuilder: azdata.FormBuilder; + private sourceIsDacpac: boolean; + private targetIsDacpac: boolean; + private database: string; + public dialogName: string; + + protected initializeDialog(): void { + this.schemaCompareTab = azdata.window.createTab(SchemaCompareLabel); + this.initializeSchemaCompareTab(); + this.dialog.content = [this.schemaCompareTab]; + } + + public openDialog(p: any, dialogName?: string): void { + let profile = p ? p.connectionProfile : undefined; + if (profile) { + this.database = profile.databaseName; + } + + let event = dialogName ? dialogName : null; + this.dialog = azdata.window.createModelViewDialog(SchemaCompareLabel, event); + + this.initializeDialog(); + + this.dialog.okButton.label = CompareButtonText; + this.dialog.okButton.onClick(async () => await this.execute()); + + this.dialog.cancelButton.label = CancelButtonText; + this.dialog.cancelButton.onClick(async () => await this.cancel()); + + azdata.window.openDialog(this.dialog); + } + + protected async execute(): Promise { + let sourceName: string; + let targetName: string; + + let sourceEndpointInfo: azdata.SchemaCompareEndpointInfo; + if (this.sourceIsDacpac) { + sourceName = this.sourceTextBox.value; + sourceEndpointInfo = { + endpointType: azdata.SchemaCompareEndpointType.dacpac, + databaseName: '', + ownerUri: '', + packageFilePath: this.sourceTextBox.value + }; + } else { + sourceName = (this.sourceServerDropdown.value as ConnectionDropdownValue).name + '.' + (this.sourceDatabaseDropdown.value).name; + let ownerUri = await azdata.connection.getUriForConnection((this.sourceServerDropdown.value as ConnectionDropdownValue).connection.connectionId); + + sourceEndpointInfo = { + endpointType: azdata.SchemaCompareEndpointType.database, + databaseName: (this.sourceDatabaseDropdown.value).name, + ownerUri: ownerUri, + packageFilePath: '' + }; + } + + let targetEndpointInfo: azdata.SchemaCompareEndpointInfo; + if (this.targetIsDacpac) { + targetName = this.targetTextBox.value; + targetEndpointInfo = { + endpointType: azdata.SchemaCompareEndpointType.dacpac, + databaseName: '', + ownerUri: '', + packageFilePath: this.targetTextBox.value + }; + } else { + targetName = (this.targetServerDropdown.value as ConnectionDropdownValue).name + '.' + (this.targetDatabaseDropdown.value).name; + let ownerUri = await azdata.connection.getUriForConnection((this.targetServerDropdown.value as ConnectionDropdownValue).connection.connectionId); + + targetEndpointInfo = { + endpointType: azdata.SchemaCompareEndpointType.database, + databaseName: (this.targetDatabaseDropdown.value).name, + ownerUri: ownerUri, + packageFilePath: '' + }; + } + + let schemaCompareResult = new SchemaCompareResult(sourceName, targetName, sourceEndpointInfo, targetEndpointInfo); + schemaCompareResult.start(); + } + + protected async cancel(): Promise { + } + + private initializeSchemaCompareTab(): void { + this.schemaCompareTab.registerContent(async view => { + this.sourceTextBox = view.modelBuilder.inputBox().withProperties({ + width: 275 + }).component(); + + this.targetTextBox = view.modelBuilder.inputBox().withProperties({ + width: 275 + }).component(); + + this.sourceServerComponent = await this.createSourceServerDropdown(view); + await this.populateServerDropdown(false); + + this.sourceDatabaseComponent = await this.createSourceDatabaseDropdown(view); + if ((this.sourceServerDropdown.value as ConnectionDropdownValue)) { + await this.populateDatabaseDropdown((this.sourceServerDropdown.value as ConnectionDropdownValue).connection.connectionId, false); + } + + this.targetServerComponent = await this.createTargetServerDropdown(view); + await this.populateServerDropdown(true); + + this.targetDatabaseComponent = await this.createTargetDatabaseDropdown(view); + if ((this.targetServerDropdown.value as ConnectionDropdownValue)) { + await this.populateDatabaseDropdown((this.targetServerDropdown.value as ConnectionDropdownValue).connection.connectionId, true); + } + + this.sourceDacpacComponent = await this.createFileBrowser(view, false); + this.targetDacpacComponent = await this.createFileBrowser(view, true); + + let sourceRadioButtons = await this.createSourceRadiobuttons(view); + let targetRadioButtons = await this.createTargetRadiobuttons(view); + + this.sourceNoActiveConnectionsText = await this.createNoActiveConnectionsText(view); + this.targetNoActiveConnectionsText = await this.createNoActiveConnectionsText(view); + + // if schema compare was launched from a db context menu, set that db as the source + if (this.database) { + this.formBuilder = view.modelBuilder.formContainer() + .withFormItems([ + sourceRadioButtons, + this.sourceServerComponent, + this.sourceDatabaseComponent, + targetRadioButtons, + this.targetDacpacComponent + ], { + horizontal: true + }); + } else { + this.formBuilder = view.modelBuilder.formContainer() + .withFormItems([ + sourceRadioButtons, + this.sourceDacpacComponent, + targetRadioButtons, + this.targetDacpacComponent + ], { + horizontal: true + }); + } + let formModel = this.formBuilder.component(); + await view.initializeModel(formModel); + }); + } + + private async createFileBrowser(view: azdata.ModelView, isTarget: boolean): Promise { + let currentTextbox = isTarget ? this.targetTextBox : this.sourceTextBox; + if (isTarget) { + this.targetFileButton = view.modelBuilder.button().withProperties({ + label: '•••', + }).component(); + } else { + this.sourceFileButton = view.modelBuilder.button().withProperties({ + label: '•••', + }).component(); + } + + let currentButton = isTarget ? this.targetFileButton : this.sourceFileButton; + + currentButton.onDidClick(async (click) => { + let fileUris = await vscode.window.showOpenDialog( + { + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + defaultUri: vscode.Uri.file(os.homedir()), + openLabel: localize('schemaCompare.openFile', 'Open'), + filters: { + 'dacpac Files': ['dacpac'], + } + } + ); + + if (!fileUris || fileUris.length === 0) { + return; + } + + let fileUri = fileUris[0]; + currentTextbox.value = fileUri.fsPath; + }); + + return { + component: currentTextbox, + title: isTarget ? TargetTextBoxLabel : SourceTextBoxLabel, + actions: [currentButton] + }; + } + + private async createSourceRadiobuttons(view: azdata.ModelView): Promise { + let dacpacRadioButton = view.modelBuilder.radioButton() + .withProperties({ + name: 'source', + label: DacpacRadioButtonLabel + }).component(); + + let databaseRadioButton = view.modelBuilder.radioButton() + .withProperties({ + name: 'source', + label: DatabaseRadioButtonLabel + }).component(); + + // show dacpac file browser + dacpacRadioButton.onDidClick(() => { + this.sourceIsDacpac = true; + this.formBuilder.removeFormItem(this.sourceNoActiveConnectionsText); + this.formBuilder.removeFormItem(this.sourceServerComponent); + this.formBuilder.removeFormItem(this.sourceDatabaseComponent); + this.formBuilder.insertFormItem(this.sourceDacpacComponent, 1, { horizontal: true }); + }); + + // show server and db dropdowns or 'No active connections' text + databaseRadioButton.onDidClick(() => { + this.sourceIsDacpac = false; + if ((this.sourceServerDropdown.value as ConnectionDropdownValue)) { + this.formBuilder.insertFormItem(this.sourceServerComponent, 1, { horizontal: true, componentWidth: 300 }); + this.formBuilder.insertFormItem(this.sourceDatabaseComponent, 2, { horizontal: true, componentWidth: 300 }); + } else { + this.formBuilder.insertFormItem(this.sourceNoActiveConnectionsText, 1, { horizontal: true }); + } + this.formBuilder.removeFormItem(this.sourceDacpacComponent); + }); + + if (this.database) { + databaseRadioButton.checked = true; + this.sourceIsDacpac = false; + } else { + dacpacRadioButton.checked = true; + this.sourceIsDacpac = true; + } + let flexRadioButtonsModel = view.modelBuilder.flexContainer() + .withLayout({ flexFlow: 'column' }) + .withItems([dacpacRadioButton, databaseRadioButton] + ).component(); + + return { + component: flexRadioButtonsModel, + title: SourceRadioButtonsLabel + }; + } + + private async createTargetRadiobuttons(view: azdata.ModelView): Promise { + let dacpacRadioButton = view.modelBuilder.radioButton() + .withProperties({ + name: 'target', + label: DacpacRadioButtonLabel + }).component(); + + let databaseRadioButton = view.modelBuilder.radioButton() + .withProperties({ + name: 'target', + label: DatabaseRadioButtonLabel + }).component(); + + // show dacpac file browser + dacpacRadioButton.onDidClick(() => { + this.targetIsDacpac = true; + this.formBuilder.removeFormItem(this.targetNoActiveConnectionsText); + this.formBuilder.removeFormItem(this.targetServerComponent); + this.formBuilder.removeFormItem(this.targetDatabaseComponent); + this.formBuilder.addFormItem(this.targetDacpacComponent, { horizontal: true }); + }); + + // show server and db dropdowns or 'No active connections' text + databaseRadioButton.onDidClick(() => { + this.targetIsDacpac = false; + this.formBuilder.removeFormItem(this.targetDacpacComponent); + if ((this.targetServerDropdown.value as ConnectionDropdownValue)) { + this.formBuilder.addFormItem(this.targetServerComponent, { horizontal: true, componentWidth: 300 }); + this.formBuilder.addFormItem(this.targetDatabaseComponent, { horizontal: true, componentWidth: 300 }); + } else { + this.formBuilder.addFormItem(this.targetNoActiveConnectionsText, { horizontal: true }); + } + }); + + dacpacRadioButton.checked = true; + this.targetIsDacpac = true; + let flexRadioButtonsModel = view.modelBuilder.flexContainer() + .withLayout({ flexFlow: 'column' }) + .withItems([dacpacRadioButton, databaseRadioButton] + ).component(); + + return { + component: flexRadioButtonsModel, + title: TargetRadioButtonsLabel + }; + } + + protected async createSourceServerDropdown(view: azdata.ModelView): Promise { + this.sourceServerDropdown = view.modelBuilder.dropDown().component(); + this.sourceServerDropdown.onValueChanged(async () => { + await this.populateDatabaseDropdown((this.sourceServerDropdown.value as ConnectionDropdownValue).connection.connectionId, false); + }); + + return { + component: this.sourceServerDropdown, + title: localize('schemaCompare.sourceServerDropdownTitle', 'Source Server') + }; + } + + protected async createTargetServerDropdown(view: azdata.ModelView): Promise { + this.targetServerDropdown = view.modelBuilder.dropDown().component(); + this.targetServerDropdown.onValueChanged(async () => { + await this.populateDatabaseDropdown((this.targetServerDropdown.value as ConnectionDropdownValue).connection.connectionId, true); + }); + + return { + component: this.targetServerDropdown, + title: localize('schemaCompare.targetServerDropdownTitle', 'Target Server') + }; + } + + protected async populateServerDropdown(isTarget: boolean): Promise { + let currentDropdown = isTarget ? this.targetServerDropdown : this.sourceServerDropdown; + let values = await this.getServerValues(); + + currentDropdown.updateProperties({ + values: values + }); + } + + protected async getServerValues(): Promise<{ connection: azdata.connection.Connection, displayName: string, name: string }[]> { + let cons = await azdata.connection.getActiveConnections(); + // This user has no active connections + if (!cons || cons.length === 0) { + return undefined; + } + + let values = cons.map(c => { + let db = c.options.databaseDisplayName; + let usr = c.options.user; + let srv = c.options.server; + + if (!db) { + db = ''; + } + + if (!usr) { + usr = 'default'; + } + + let finalName = `${srv}, ${db} (${usr})`; + return { + connection: c, + displayName: finalName, + name: srv + }; + }); + + return values; + } + + protected async createSourceDatabaseDropdown(view: azdata.ModelView): Promise { + this.sourceDatabaseDropdown = view.modelBuilder.dropDown().component(); + + return { + component: this.sourceDatabaseDropdown, + title: localize('schemaCompare.sourceDatabaseDropdownTitle', 'Source Database') + }; + } + + protected async createTargetDatabaseDropdown(view: azdata.ModelView): Promise { + this.targetDatabaseDropdown = view.modelBuilder.dropDown().component(); + + return { + component: this.targetDatabaseDropdown, + title: localize('schemaCompare.targetDatabaseDropdownTitle', 'Target Database') + }; + } + + protected async populateDatabaseDropdown(connectionId: string, isTarget: boolean): Promise { + let currentDropdown = isTarget ? this.targetDatabaseDropdown : this.sourceDatabaseDropdown; + currentDropdown.updateProperties({ values: [] }); + + let values = await this.getDatabaseValues(connectionId); + currentDropdown.updateProperties({ + values: values + }); + } + + protected async getDatabaseValues(connectionId: string): Promise<{ displayName, name }[]> { + let idx = -1; + let count = -1; + let values = (await azdata.connection.listDatabases(connectionId)).map(db => { + count++; + // if schema compare was launched from a db context menu, set that db at the top of the dropdown + if (this.database && db === this.database) { + idx = count; + } + + return { + displayName: db, + name: db + }; + }); + + if (idx >= 0) { + let tmp = values[0]; + values[0] = values[idx]; + values[idx] = tmp; + } + return values; + } + + protected async createNoActiveConnectionsText(view: azdata.ModelView): Promise { + let noActiveConnectionsText = view.modelBuilder.text().withProperties({ value: NoActiveConnectionsLabel }).component(); + + return { + component: noActiveConnectionsText, + title: '' + }; + } +} + +interface ConnectionDropdownValue extends azdata.CategoryValue { + connection: azdata.connection.Connection; +} \ No newline at end of file diff --git a/extensions/schema-compare/src/main.ts b/extensions/schema-compare/src/main.ts new file mode 100644 index 0000000000..5e17966ec3 --- /dev/null +++ b/extensions/schema-compare/src/main.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +import * as vscode from 'vscode'; +import MainController from './controllers/mainController'; + +let controllers: MainController[] = []; + +export async function activate(context: vscode.ExtensionContext): Promise { + // Start the main controller + let mainController = new MainController(context); + controllers.push(mainController); + context.subscriptions.push(mainController); + + await mainController.activate(); +} + +export function deactivate(): void { + for (let controller of controllers) { + controller.deactivate(); + } +} diff --git a/extensions/schema-compare/src/media/compare-inverse.svg b/extensions/schema-compare/src/media/compare-inverse.svg new file mode 100644 index 0000000000..3cae2ff14b --- /dev/null +++ b/extensions/schema-compare/src/media/compare-inverse.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/extensions/schema-compare/src/media/compare.svg b/extensions/schema-compare/src/media/compare.svg new file mode 100644 index 0000000000..7ad2cff30d --- /dev/null +++ b/extensions/schema-compare/src/media/compare.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/extensions/schema-compare/src/media/generate-script-inverse.svg b/extensions/schema-compare/src/media/generate-script-inverse.svg new file mode 100644 index 0000000000..7f6545b590 --- /dev/null +++ b/extensions/schema-compare/src/media/generate-script-inverse.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/schema-compare/src/media/generate-script.svg b/extensions/schema-compare/src/media/generate-script.svg new file mode 100644 index 0000000000..a279251e92 --- /dev/null +++ b/extensions/schema-compare/src/media/generate-script.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/schema-compare/src/media/switch-directions-inverse.svg b/extensions/schema-compare/src/media/switch-directions-inverse.svg new file mode 100644 index 0000000000..15c31975ef --- /dev/null +++ b/extensions/schema-compare/src/media/switch-directions-inverse.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/schema-compare/src/media/switch-directions.svg b/extensions/schema-compare/src/media/switch-directions.svg new file mode 100644 index 0000000000..e9edd4575f --- /dev/null +++ b/extensions/schema-compare/src/media/switch-directions.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/schema-compare/src/schemaCompareResult.ts b/extensions/schema-compare/src/schemaCompareResult.ts new file mode 100644 index 0000000000..c2f740dfac --- /dev/null +++ b/extensions/schema-compare/src/schemaCompareResult.ts @@ -0,0 +1,366 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as nls from 'vscode-nls'; +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; +import * as os from 'os'; +import * as path from 'path'; +const localize = nls.loadMessageBundle(); + +export class SchemaCompareResult { + private differencesTable: azdata.TableComponent; + private loader: azdata.LoadingComponent; + private editor: azdata.workspace.ModelViewEditor; + private diffEditor: azdata.DiffEditorComponent; + private splitView: azdata.SplitViewContainer; + private flexModel: azdata.FlexContainer; + private noDifferencesLabel: azdata.TextComponent; + private sourceTargetFlexLayout: azdata.FlexContainer; + private switchButton: azdata.ButtonComponent; + private compareButton: azdata.ButtonComponent; + private generateScriptButton: azdata.ButtonComponent; + private SchemaCompareActionMap: Map; + private comparisonResult: azdata.SchemaCompareResult; + private sourceNameComponent: azdata.TableComponent; + private targetNameComponent: azdata.TableComponent; + + constructor(private sourceName: string, private targetName: string, private sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, private targetEndpointInfo: azdata.SchemaCompareEndpointInfo) { + this.SchemaCompareActionMap = new Map(); + this.SchemaCompareActionMap[azdata.SchemaUpdateAction.Delete] = localize('schemaCompare.deleteAction', 'Delete'); + this.SchemaCompareActionMap[azdata.SchemaUpdateAction.Change] = localize('schemaCompare.changeAction', 'Change'); + this.SchemaCompareActionMap[azdata.SchemaUpdateAction.Add] = localize('schemaCompare.addAction', 'Add'); + + this.editor = azdata.workspace.createModelViewEditor(localize('schemaCompare.Title', 'Schema Compare'), { retainContextWhenHidden: true, supportsSave: true }); + + this.editor.registerContent(async view => { + this.differencesTable = view.modelBuilder.table().withProperties({ + data: [], + height: 300, + }).component(); + + this.diffEditor = view.modelBuilder.diffeditor().withProperties({ + contentLeft: os.EOL, + contentRight: os.EOL, + height: 500, + title: localize('schemaCompare.ObjectDefinitionsTitle', 'Object Definitions') + }).component(); + + this.splitView = view.modelBuilder.splitViewContainer().component(); + + let sourceTargetLabels = view.modelBuilder.flexContainer() + .withProperties({ + alignItems: 'stretch', + horizontal: true + }).component(); + + this.sourceTargetFlexLayout = view.modelBuilder.flexContainer() + .withProperties({ + alignItems: 'stretch', + horizontal: true + }).component(); + + this.createSwitchButton(view); + this.createCompareButton(view); + this.createGenerateScriptButton(view); + this.resetButtons(); + + let toolBar = view.modelBuilder.toolbarContainer(); + toolBar.addToolbarItems([{ + component: this.compareButton + }, { + component: this.generateScriptButton, + toolbarSeparatorAfter: true + }, + { + component: this.switchButton + }]); + + let sourceLabel = view.modelBuilder.text().withProperties({ + value: localize('schemaCompare.sourceLabel', 'Source') + }).component(); + + let targetLabel = view.modelBuilder.text().withProperties({ + value: localize('schemaCompare.targetLabel', 'Target') + }).component(); + + let arrowLabel = view.modelBuilder.text().withProperties({ + value: localize('schemaCompare.switchLabel', '➔') + }).component(); + + this.sourceNameComponent = view.modelBuilder.table().withProperties({ + columns: [ + { + value: sourceName, + headerCssClass: 'no-borders', + toolTip: sourceName + }, + ] + }).component(); + + this.targetNameComponent = view.modelBuilder.table().withProperties({ + columns: [ + { + value: targetName, + headerCssClass: 'no-borders', + toolTip: targetName + }, + ] + }).component(); + + sourceTargetLabels.addItem(sourceLabel, { CSSStyles: { 'width': '55%', 'margin-left': '15px', 'font-size': 'larger', 'font-weight': 'bold' } }); + sourceTargetLabels.addItem(targetLabel, { CSSStyles: { 'width': '45%', 'font-size': 'larger', 'font-weight': 'bold' } }); + this.sourceTargetFlexLayout.addItem(this.sourceNameComponent, { CSSStyles: { 'width': '45%', 'height': '25px', 'margin-top': '10px', 'margin-left': '15px' } }); + this.sourceTargetFlexLayout.addItem(arrowLabel, { CSSStyles: { 'width': '10%', 'font-size': 'larger', 'text-align-last': 'center' } }); + this.sourceTargetFlexLayout.addItem(this.targetNameComponent, { CSSStyles: { 'width': '45%', 'height': '25px', 'margin-top': '10px', 'margin-left': '15px' } }); + + this.loader = view.modelBuilder.loadingComponent().component(); + this.noDifferencesLabel = view.modelBuilder.text().withProperties({ + value: localize('schemaCompare.noDifferences', 'No schema differences were found') + }).component(); + + this.flexModel = view.modelBuilder.flexContainer().component(); + this.flexModel.addItem(toolBar.component(), { flex: 'none' }); + this.flexModel.addItem(sourceTargetLabels, { flex: 'none' }); + this.flexModel.addItem(this.sourceTargetFlexLayout, { flex: 'none' }); + this.flexModel.addItem(this.loader, { CSSStyles: { 'margin-top': '30px' } }); + this.flexModel.setLayout({ + flexFlow: 'column', + height: '100%' + }); + + await view.initializeModel(this.flexModel); + }); + } + + public start(): void { + this.editor.openEditor(); + this.execute(); + } + + private async execute(): Promise { + let service = await SchemaCompareResult.getService('MSSQL'); + this.comparisonResult = await service.schemaCompare(this.sourceEndpointInfo, this.targetEndpointInfo, azdata.TaskExecutionMode.execute); + if (!this.comparisonResult || !this.comparisonResult.success) { + vscode.window.showErrorMessage(localize('schemaCompare.compareErrorMessage', "Schema Compare failed: {0}", this.comparisonResult.errorMessage ? this.comparisonResult.errorMessage : 'Unknown')); + return; + } + + let data = this.getAllDifferences(this.comparisonResult.differences); + + this.differencesTable.updateProperties({ + data: data, + columns: [ + { + value: localize('schemaCompare.typeColumn', 'Type'), + cssClass: 'align-with-header', + width: 50 + }, + { + value: localize('schemaCompare.sourceNameColumn', 'Target Name'), + cssClass: 'align-with-header', + width: 90 + }, + { + value: localize('schemaCompare.actionColumn', 'Action'), + cssClass: 'align-with-header', + width: 30 + }, + { + value: localize('schemaCompare.targetNameColumn', 'Source Name'), + cssClass: 'align-with-header', + width: 150 + }] + }); + + this.splitView.addItem(this.differencesTable); + this.splitView.addItem(this.diffEditor); + this.splitView.setLayout({ + orientation: 'vertical', + splitViewHeight: 800 + }); + + this.flexModel.removeItem(this.loader); + this.switchButton.enabled = true; + this.compareButton.enabled = true; + + if (this.comparisonResult.differences.length > 0) { + this.flexModel.addItem(this.splitView); + + // only enable generate script button if the target is a db + if (this.targetEndpointInfo.endpointType === azdata.SchemaCompareEndpointType.database) { + this.generateScriptButton.enabled = true; + } else { + this.generateScriptButton.title = localize('schemaCompare.generateScriptButtonDisabledTitle', 'Generate script is enabled when the target is a database'); + } + } else { + this.flexModel.addItem(this.noDifferencesLabel, { CSSStyles: { 'margin': 'auto' } }); + } + + let sourceText = ''; + let targetText = ''; + this.differencesTable.onRowSelected(() => { + let difference = this.comparisonResult.differences[this.differencesTable.selectedRows[0]]; + if (difference !== undefined) { + sourceText = difference.sourceScript === null ? '\n' : this.getAggregatedScript(difference, true); + targetText = difference.targetScript === null ? '\n' : this.getAggregatedScript(difference, false); + + this.diffEditor.updateProperties({ + contentLeft: sourceText, + contentRight: targetText, + title: localize('schemaCompare.ObjectDefinitionsTitle', 'Object Definitions') + }); + } + }); + } + + private getAllDifferences(differences: azdata.DiffEntry[]): string[][] { + let data = []; + if (differences) { + differences.forEach(difference => { + if (difference.differenceType === azdata.SchemaDifferenceType.Object) { + if (difference.sourceValue !== null || difference.targetValue !== null) { + data.push([difference.name, difference.sourceValue, this.SchemaCompareActionMap[difference.updateAction], difference.targetValue]); + } + } + }); + } + + return data; + } + + private getAggregatedScript(diffEntry: azdata.DiffEntry, getSourceScript: boolean): string { + let script = ''; + if (diffEntry !== null) { + script += getSourceScript ? diffEntry.sourceScript : diffEntry.targetScript; + diffEntry.children.forEach(child => { + let childScript = this.getAggregatedScript(child, getSourceScript); + if (childScript !== 'null') { + script += childScript; + } + }); + } + return script; + } + + private reExecute(): void { + this.flexModel.removeItem(this.splitView); + this.flexModel.removeItem(this.noDifferencesLabel); + this.flexModel.addItem(this.loader, { CSSStyles: { 'margin-top': '30px' } }); + this.diffEditor.updateProperties({ + contentLeft: os.EOL, + contentRight: os.EOL + }); + this.differencesTable.selectedRows = null; + this.resetButtons(); + this.execute(); + } + + private createCompareButton(view: azdata.ModelView): void { + this.compareButton = view.modelBuilder.button().withProperties({ + label: localize('schemaCompare.compareButton', 'Compare'), + iconPath: { + light: path.join(__dirname, 'media', 'compare.svg'), + dark: path.join(__dirname, 'media', 'compare-inverse.svg') + }, + title: localize('schemaCompare.compareButtonTitle', 'Compare') + }).component(); + + this.compareButton.onDidClick(async (click) => { + this.reExecute(); + }); + } + + private createGenerateScriptButton(view: azdata.ModelView): void { + this.generateScriptButton = view.modelBuilder.button().withProperties({ + label: localize('schemaCompare.generateScriptButton', 'Generate script'), + iconPath: { + light : path.join(__dirname, 'media', 'generate-script.svg'), + dark: path.join(__dirname, 'media', 'generate-script-inverse.svg') + }, + }).component(); + + this.generateScriptButton.onDidClick(async (click) => { + // get file path + let now = new Date(); + let datetime = now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate() + '-' + now.getHours() + '-' + now.getMinutes() + '-' + now.getSeconds(); + let defaultFilePath = path.join(os.homedir(), this.targetName + '_Update_' + datetime + '.sql'); + let fileUri = await vscode.window.showSaveDialog( + { + defaultUri: vscode.Uri.file(defaultFilePath), + saveLabel: localize('schemaCompare.saveFile', 'Save'), + filters: { + 'SQL Files': ['sql'], + } + } + ); + + if (!fileUri) { + return; + } + + let service = await SchemaCompareResult.getService('MSSQL'); + let result = await service.schemaCompareGenerateScript(this.comparisonResult.operationId, this.targetEndpointInfo.databaseName, fileUri.fsPath, azdata.TaskExecutionMode.execute); + if (!result || !result.success) { + vscode.window.showErrorMessage( + localize('schemaCompare.generateScriptErrorMessage', "Generate script failed: '{0}'", (result && result.errorMessage) ? result.errorMessage : 'Unknown')); + } + }); + } + + private resetButtons(): void { + this.compareButton.enabled = false; + this.switchButton.enabled = false; + this.generateScriptButton.enabled = false; + this.generateScriptButton.title = localize('schemaCompare.generateScriptEnabledButton', 'Generate script to deploy changes to target'); + } + + private createSwitchButton(view: azdata.ModelView): void { + let swapIcon = path.join(__dirname, 'media', 'switch-directions.svg'); + + this.switchButton = view.modelBuilder.button().withProperties({ + label: localize('schemaCompare.switchDirectionButton', 'Switch direction'), + iconPath: { + light : path.join(__dirname, 'media', 'switch-directions.svg'), + dark: path.join(__dirname, 'media', 'switch-directions-inverse.svg') + }, + title: localize('schemaCompare.switchButtonTitle', 'Switch source and target') + }).component(); + + this.switchButton.onDidClick(async (click) => { + // switch source and target + [this.sourceEndpointInfo, this.targetEndpointInfo] = [this.targetEndpointInfo, this.sourceEndpointInfo]; + [this.sourceName, this.targetName] = [this.targetName, this.sourceName]; + + this.sourceNameComponent.updateProperties({ + columns: [ + { + value: this.sourceName, + headerCssClass: 'no-borders', + toolTip: this.sourceName + }, + ] + }); + + this.targetNameComponent.updateProperties({ + columns: [ + { + value: this.targetName, + headerCssClass: 'no-borders', + toolTip: this.targetName + }, + ] + }); + + this.reExecute(); + }); + } + + private static async getService(providerName: string): Promise { + let service = azdata.dataprotocol.getProvider(providerName, azdata.DataProviderType.SchemaCompareServicesProvider); + return service; + } +} \ No newline at end of file diff --git a/extensions/schema-compare/src/typings/ref.d.ts b/extensions/schema-compare/src/typings/ref.d.ts new file mode 100644 index 0000000000..4d46be908b --- /dev/null +++ b/extensions/schema-compare/src/typings/ref.d.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// +/// +/// +/// \ No newline at end of file diff --git a/extensions/schema-compare/tsconfig.json b/extensions/schema-compare/tsconfig.json new file mode 100644 index 0000000000..76b02da52f --- /dev/null +++ b/extensions/schema-compare/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "./out", + "lib": [ + "es6", + "es2015.promise" + ], + "sourceMap": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "declaration": false + }, + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/extensions/schema-compare/yarn.lock b/extensions/schema-compare/yarn.lock new file mode 100644 index 0000000000..6ac1974f1b --- /dev/null +++ b/extensions/schema-compare/yarn.lock @@ -0,0 +1,46 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +applicationinsights@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.0.1.tgz#53446b830fe8d5d619eee2a278b31d3d25030927" + integrity sha1-U0Rrgw/o1dYZ7uKieLMdPSUDCSc= + dependencies: + diagnostic-channel "0.2.0" + diagnostic-channel-publishers "0.2.1" + zone.js "0.7.6" + +diagnostic-channel-publishers@0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz#8e2d607a8b6d79fe880b548bc58cc6beb288c4f3" + integrity sha1-ji1geottef6IC1SLxYzGvrKIxPM= + +diagnostic-channel@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz#cc99af9612c23fb1fff13612c72f2cbfaa8d5a17" + integrity sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc= + dependencies: + semver "^5.3.0" + +semver@^5.3.0: + version "5.7.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" + integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== + +vscode-extension-telemetry@0.0.18: + version "0.0.18" + resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.0.18.tgz#602ba20d8c71453aa34533a291e7638f6e5c0327" + integrity sha512-Vw3Sr+dZwl+c6PlsUwrTtCOJkgrmvS3OUVDQGcmpXWAgq9xGq6as0K4pUx+aGqTjzLAESmWSrs6HlJm6J6Khcg== + dependencies: + applicationinsights "1.0.1" + +vscode-nls@^3.2.1: + version "3.2.5" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.5.tgz#25520c1955108036dec607c85e00a522f247f1a4" + integrity sha512-ITtoh3V4AkWXMmp3TB97vsMaHRgHhsSFPsUdzlueSL+dRZbSNTZeOmdQv60kjCV306ghPxhDeoNUEm3+EZMuyw== + +zone.js@0.7.6: + version "0.7.6" + resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.7.6.tgz#fbbc39d3e0261d0986f1ba06306eb3aeb0d22009" + integrity sha1-+7w50+AmHQmG8boGMG6zrrDSIAk= diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 342fd7e8e0..15a0158363 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -44,6 +44,8 @@ declare module 'azdata' { export function registerDacFxServicesProvider(provider: DacFxServicesProvider): vscode.Disposable; + export function registerSchemaCompareServicesProvider(provider: SchemaCompareServicesProvider): vscode.Disposable; + /** * An [event](#Event) which fires when the specific flavor of a language used in DMP * connections has changed. And example is for a SQL connection, the flavor changes @@ -1699,6 +1701,51 @@ declare module 'azdata' { generateDeployPlan(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: TaskExecutionMode): Thenable; } + // Schema Compare interfaces ----------------------------------------------------------------------- + export interface SchemaCompareResult extends ResultStatus { + operationId: string; + areEqual: boolean; + differences: DiffEntry[]; + } + + export interface DiffEntry { + updateAction: SchemaUpdateAction; + differenceType: SchemaDifferenceType; + name: string; + sourceValue: string; + targetValue: string; + parent: DiffEntry; + children: DiffEntry[]; + sourceScript: string; + targetScript: string; + } + + export enum SchemaUpdateAction { + Delete = 0, + Change = 1, + Add = 2 + } + + export enum SchemaDifferenceType { + Object = 0, + Property = 1 + } + export enum SchemaCompareEndpointType { + database = 0, + dacpac = 1 + } + export interface SchemaCompareEndpointInfo { + endpointType: SchemaCompareEndpointType; + packageFilePath: string; + databaseName: string; + ownerUri: string; + } + + export interface SchemaCompareServicesProvider extends DataProvider { + schemaCompare(sourceEndpointInfo: SchemaCompareEndpointInfo, targetEndpointInfo: SchemaCompareEndpointInfo, taskExecutionMode: TaskExecutionMode): Thenable; + schemaCompareGenerateScript(operationId: string, targetDatabaseName: string, scriptFilePath: string, taskExecutionMode: TaskExecutionMode): Thenable; + } + // Security service interfaces ------------------------------------------------------------------------ export interface CredentialInfo { id: number; @@ -2916,6 +2963,7 @@ declare module 'azdata' { value: string; width?: number; cssClass?: string; + headerCssClass?: string; toolTip?: string; } @@ -3764,6 +3812,7 @@ declare module 'azdata' { AgentServicesProvider = 'AgentServicesProvider', CapabilitiesProvider = 'CapabilitiesProvider', DacFxServicesProvider = 'DacFxServicesProvider', + SchemaCompareServicesProvider = 'SchemaCompareServicesProvider', ObjectExplorerNodeProvider = 'ObjectExplorerNodeProvider', } diff --git a/src/sql/parts/modelComponents/diffeditor.component.ts b/src/sql/parts/modelComponents/diffeditor.component.ts index 80318bb1da..1cea0cf5e0 100644 --- a/src/sql/parts/modelComponents/diffeditor.component.ts +++ b/src/sql/parts/modelComponents/diffeditor.component.ts @@ -30,7 +30,7 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; @Component({ template: `
-
+
{{_title}}
`, @@ -71,6 +71,7 @@ export default class DiffEditorComponent extends ComponentBase implements ICompo private _createEditor(): void { this._instantiationService = this._instantiationService.createChild(new ServiceCollection([IProgressService, new SimpleProgressService()])); this._editor = this._instantiationService.createInstance(TextDiffEditor); + this._editor.reverseColoring(); this._editor.create(this._el.nativeElement); this._editor.setVisible(true); let uri1 = this.createUri('source'); diff --git a/src/sql/parts/modelComponents/editor.css b/src/sql/parts/modelComponents/editor.css index a78d4fccc8..250ab7314d 100644 --- a/src/sql/parts/modelComponents/editor.css +++ b/src/sql/parts/modelComponents/editor.css @@ -13,4 +13,12 @@ modelview-diff-editor-component { height: 100%; width : 100%; display: block; +} + +.vs-dark modelview-diff-editor-title { + background: #444444; +} + +modelview-diff-editor-title { + background: #f4f4f4; } \ No newline at end of file diff --git a/src/sql/parts/modelComponents/table.component.ts b/src/sql/parts/modelComponents/table.component.ts index 8c5f6f2146..fed80a9a50 100644 --- a/src/sql/parts/modelComponents/table.component.ts +++ b/src/sql/parts/modelComponents/table.component.ts @@ -58,6 +58,7 @@ export default class TableComponent extends ComponentBase implements IComponent, field: col.value, width: col.width, cssClass: col.cssClass, + headerCssClass: col.headerCssClass, toolTip: col.toolTip }; } else { diff --git a/src/sql/parts/modelComponents/table.css b/src/sql/parts/modelComponents/table.css index 58321ba2b4..109a7f7053 100644 --- a/src/sql/parts/modelComponents/table.css +++ b/src/sql/parts/modelComponents/table.css @@ -11,4 +11,9 @@ .align-with-header { padding-left:3px !important; +} + +.no-borders +{ + border: none !important } \ No newline at end of file diff --git a/src/sql/platform/schemaCompare/common/schemaCompareService.ts b/src/sql/platform/schemaCompare/common/schemaCompareService.ts new file mode 100644 index 0000000000..b3d3033553 --- /dev/null +++ b/src/sql/platform/schemaCompare/common/schemaCompareService.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as azdata from 'azdata'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; +import { localize } from 'vs/nls'; + +export const SERVICE_ID = 'SchemaCompareService'; +export const ISchemaCompareService = createDecorator(SERVICE_ID); + +export interface ISchemaCompareService { + _serviceBrand: any; + + registerProvider(providerId: string, provider: azdata.SchemaCompareServicesProvider): void; + schemaCompare(sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode): void; + schemaCompareGenerateScript(operationId: string, targetDatabaseName: string, scriptFilePath: string, taskExecutionMode: azdata.TaskExecutionMode): void; +} + +export class SchemaCompareService implements ISchemaCompareService { + _serviceBrand: any; + private _providers: { [handle: string]: azdata.SchemaCompareServicesProvider; } = Object.create(null); + + constructor(@IConnectionManagementService private _connectionService: IConnectionManagementService) { } + + registerProvider(providerId: string, provider: azdata.SchemaCompareServicesProvider): void { + this._providers[providerId] = provider; + } + + schemaCompare(sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode): Thenable { + return this._runAction(sourceEndpointInfo.ownerUri, (runner) => { + return runner.schemaCompare(sourceEndpointInfo, targetEndpointInfo, taskExecutionMode); + }); + } + + schemaCompareGenerateScript(operationId: string, targetDatabaseName: string, scriptFilePath: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable { + return this._runAction('', (runner) => { + return runner.schemaCompareGenerateScript(operationId, targetDatabaseName, scriptFilePath, taskExecutionMode); + }); + } + + private _runAction(uri: string, action: (handler: azdata.SchemaCompareServicesProvider) => Thenable): Thenable { + let providerId: string = this._connectionService.getProviderIdFromUri(uri); + + if (!providerId) { + return Promise.reject(new Error(localize('providerIdNotValidError', "Connection is required in order to interact with SchemaCompareService"))); + } + let handler = this._providers[providerId]; + if (handler) { + return action(handler); + } else { + return Promise.reject(new Error(localize('noHandlerRegistered', "No Handler Registered"))); + } + } +} \ No newline at end of file diff --git a/src/sql/sqlops.d.ts b/src/sql/sqlops.d.ts index 0a15d2341c..da331c72d2 100644 --- a/src/sql/sqlops.d.ts +++ b/src/sql/sqlops.d.ts @@ -39,6 +39,7 @@ declare module 'sqlops' { export function registerDacFxServicesProvider(provider: DacFxServicesProvider): vscode.Disposable; + /** * An [event](#Event) which fires when the specific flavor of a language used in DMP * connections has changed. And example is for a SQL connection, the flavor changes @@ -1324,6 +1325,7 @@ declare module 'sqlops' { generateDeployPlan(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: TaskExecutionMode): Thenable; } + // Security service interfaces ------------------------------------------------------------------------ export interface CredentialInfo { id: number; diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index 622466a712..ac8b441899 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -1602,6 +1602,7 @@ declare module 'sqlops' { AgentServicesProvider = 'AgentServicesProvider', CapabilitiesProvider = 'CapabilitiesProvider', DacFxServicesProvider = 'DacFxServicesProvider', + SchemaCompareServicesProvider = 'SchemaCompareServicesProvider', ObjectExplorerNodeProvider = 'ObjectExplorerNodeProvider' } diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index e7662b03cb..a2a9ee55d0 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -292,6 +292,7 @@ export enum DataProviderType { AgentServicesProvider = 'AgentServicesProvider', CapabilitiesProvider = 'CapabilitiesProvider', DacFxServicesProvider = 'DacFxServicesProvider', + SchemaCompareServicesProvider = 'SchemaCompareServicesProvider', ObjectExplorerNodeProvider = 'ObjectExplorerNodeProvider' } @@ -547,3 +548,19 @@ export class ConnectionProfile { return undefined; } } + +export enum SchemaUpdateAction { + Delete = 0, + Change = 1, + Add = 2 +} + + export enum SchemaDifferenceType { + Object = 0, + Property = 1 +} + + export enum SchemaCompareEndpointType { + database = 0, + dacpac = 1 +} diff --git a/src/sql/workbench/api/node/extHostDataProtocol.ts b/src/sql/workbench/api/node/extHostDataProtocol.ts index 600b0c222f..2e184f5ece 100644 --- a/src/sql/workbench/api/node/extHostDataProtocol.ts +++ b/src/sql/workbench/api/node/extHostDataProtocol.ts @@ -167,6 +167,12 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape { return rt; } + $registerSchemaCompareServiceProvider(provider: azdata.SchemaCompareServicesProvider): vscode.Disposable { + let rt = this.registerProvider(provider, DataProviderType.SchemaCompareServicesProvider); + this._proxy.$registerSchemaCompareServicesProvider(provider.providerId, provider.handle); + return rt; + } + // Capabilities Discovery handlers $getServerCapabilities(handle: number, client: azdata.DataProtocolClientCapabilities): Thenable { return this._resolveProvider(handle).getServerCapabilities(client); diff --git a/src/sql/workbench/api/node/mainThreadDataProtocol.ts b/src/sql/workbench/api/node/mainThreadDataProtocol.ts index c2cf3b71f0..f4229c382f 100644 --- a/src/sql/workbench/api/node/mainThreadDataProtocol.ts +++ b/src/sql/workbench/api/node/mainThreadDataProtocol.ts @@ -26,6 +26,7 @@ import { ISerializationService } from 'sql/platform/serialization/common/seriali import { IFileBrowserService } from 'sql/platform/fileBrowser/common/interfaces'; import { IExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; import { IDacFxService } from 'sql/platform/dacfx/common/dacFxService'; +import { ISchemaCompareService } from 'sql/platform/schemaCompare/common/schemaCompareService'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; /** @@ -57,6 +58,7 @@ export class MainThreadDataProtocol implements MainThreadDataProtocolShape { @ISerializationService private _serializationService: ISerializationService, @IFileBrowserService private _fileBrowserService: IFileBrowserService, @IDacFxService private _dacFxService: IDacFxService, + @ISchemaCompareService private _schemaCompareService: ISchemaCompareService, ) { if (extHostContext) { this._proxy = extHostContext.getProxy(SqlExtHostContext.ExtHostDataProtocol); @@ -453,6 +455,20 @@ export class MainThreadDataProtocol implements MainThreadDataProtocolShape { return undefined; } + public $registerSchemaCompareServicesProvider(providerId: string, handle: number): Promise { + const self = this; + this._schemaCompareService.registerProvider(providerId, { + schemaCompare(sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode): Thenable { + return self._proxy.$schemaCompare(handle, sourceEndpointInfo, targetEndpointInfo, taskExecutionMode); + }, + schemaCompareGenerateScript(operationId: string, targetDatabaseName: string, scriptFilePath: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable { + return self._proxy.$schemaCompareGenerateScript(handle, operationId, targetDatabaseName, scriptFilePath, taskExecutionMode); + } + }); + + return undefined; + } + // Connection Management handlers public $onConnectionComplete(handle: number, connectionInfoSummary: azdata.ConnectionInfoSummary): void { this._connectionManagementService.onConnectionComplete(handle, connectionInfoSummary); diff --git a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts index 9aab0715c3..74f93700fb 100644 --- a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts @@ -354,6 +354,10 @@ export function createApiFactory( return extHostDataProvider.$registerDacFxServiceProvider(provider); }; + let registerSchemaCompareServicesProvider = (provider: azdata.SchemaCompareServicesProvider): vscode.Disposable => { + return extHostDataProvider.$registerSchemaCompareServiceProvider(provider); + }; + // namespace: dataprotocol const dataprotocol: typeof azdata.dataprotocol = { registerBackupProvider, @@ -371,6 +375,7 @@ export function createApiFactory( registerAgentServicesProvider, registerCapabilitiesServiceProvider, registerDacFxServicesProvider, + registerSchemaCompareServicesProvider, onDidChangeLanguageFlavor(listener: (e: azdata.DidChangeLanguageFlavorParams) => any, thisArgs?: any, disposables?: extHostTypes.Disposable[]) { return extHostDataProvider.onDidChangeLanguageFlavor(listener, thisArgs, disposables); }, @@ -528,7 +533,10 @@ export function createApiFactory( nb: nb, AzureResource: sqlExtHostTypes.AzureResource, TreeItem: sqlExtHostTypes.TreeItem, - extensions: extensions + extensions: extensions, + SchemaUpdateAction: sqlExtHostTypes.SchemaUpdateAction, + SchemaDifferenceType: sqlExtHostTypes.SchemaDifferenceType, + SchemaCompareEndpointType: sqlExtHostTypes.SchemaCompareEndpointType }; }, @@ -754,6 +762,7 @@ export function createApiFactory( return extHostDataProvider.$registerDacFxServiceProvider(provider); }; + // namespace: dataprotocol const dataprotocol: typeof sqlops.dataprotocol = { registerBackupProvider, diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index 58dedf080f..16a5ca3b6c 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -455,6 +455,15 @@ export abstract class ExtHostDataProtocolShape { */ $generateDeployPlan(handle: number, packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable { throw ni(); } + /** + * Schema compare + */ + $schemaCompare(handle: number, sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode): Thenable { throw ni(); } + + /** + * Schema compare generate script + */ + $schemaCompareGenerateScript(handle: number, operationId: string, targetDatabaseName: string, scriptFilePath: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable { throw ni(); } } /** @@ -524,6 +533,7 @@ export interface MainThreadDataProtocolShape extends IDisposable { $registerAdminServicesProvider(providerId: string, handle: number): Promise; $registerAgentServicesProvider(providerId: string, handle: number): Promise; $registerDacFxServicesProvider(providerId: string, handle: number): Promise; + $registerSchemaCompareServicesProvider(providerId: string, handle: number): Promise; $unregisterProvider(handle: number): Promise; $onConnectionComplete(handle: number, connectionInfoSummary: azdata.ConnectionInfoSummary): void; $onIntelliSenseCacheComplete(handle: number, connectionUri: string): void; diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index ccb722f5df..1420d6891e 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -195,6 +195,7 @@ import { IAdminService, AdminService } from 'sql/workbench/services/admin/common import { IJobManagementService } from 'sql/platform/jobManagement/common/interfaces'; import { JobManagementService } from 'sql/platform/jobManagement/common/jobManagementService'; import { IDacFxService, DacFxService } from 'sql/platform/dacfx/common/dacFxService'; +import { ISchemaCompareService, SchemaCompareService } from 'sql/platform/schemaCompare/common/schemaCompareService'; import { IBackupService } from 'sql/platform/backup/common/backupService'; import { BackupService } from 'sql/platform/backup/common/backupServiceImp'; import { IBackupUiService } from 'sql/workbench/services/backup/common/backupUiService'; @@ -269,6 +270,7 @@ registerSingleton(INotebookService, NotebookService); registerSingleton(IAccountPickerService, AccountPickerService); registerSingleton(IProfilerService, ProfilerService); registerSingleton(IDacFxService, DacFxService); +registerSingleton(ISchemaCompareService, SchemaCompareService); // {{SQL CARBON EDIT}} - End //#region --- workbench parts