diff --git a/extensions/schema-compare/src/dialogs/schemaCompareDialog.ts b/extensions/schema-compare/src/dialogs/schemaCompareDialog.ts index 61ae9e9b7f..f415ac3833 100644 --- a/extensions/schema-compare/src/dialogs/schemaCompareDialog.ts +++ b/extensions/schema-compare/src/dialogs/schemaCompareDialog.ts @@ -9,7 +9,7 @@ import * as loc from '../localizedConstants'; import * as path from 'path'; import { SchemaCompareMainWindow } from '../schemaCompareMainWindow'; import { TelemetryReporter, TelemetryViews } from '../telemetry'; -import { getEndpointName, getRootPath, exists } from '../utils'; +import { getEndpointName, getRootPath, exists, getAzdataApi, getSchemaCompareEndpointString } from '../utils'; import * as mssql from '../../../mssql'; const titleFontSize: number = 13; @@ -18,13 +18,16 @@ interface Deferred { resolve: (result: T | Promise) => void; reject: (reason: any) => void; } + export class SchemaCompareDialog { public dialog: azdata.window.Dialog; public dialogName: string; + private schemaCompareTab: azdata.window.DialogTab; private sourceDacpacRadioButton: azdata.RadioButtonComponent; private sourceDatabaseRadioButton: azdata.RadioButtonComponent; - private schemaCompareTab: azdata.window.DialogTab; + private sourceProjectRadioButton: azdata.RadioButtonComponent; private sourceDacpacComponent: azdata.FormComponent; + private sourceProjectFilePathComponent: azdata.FormComponent; private sourceTextBox: azdata.InputBoxComponent; private sourceFileButton: azdata.ButtonComponent; private sourceServerComponent: azdata.FormComponent; @@ -32,22 +35,30 @@ export class SchemaCompareDialog { private sourceConnectionButton: azdata.ButtonComponent; private sourceDatabaseComponent: azdata.FormComponent; private sourceDatabaseDropdown: azdata.DropDownComponent; + private sourceEndpointType: mssql.SchemaCompareEndpointType; + private sourceDbEditable: string; + private sourceDacpacPath: string; + private sourceProjectFilePath: string; private targetDacpacComponent: azdata.FormComponent; + private targetProjectFilePathComponent: azdata.FormComponent; + private targetProjectStructureComponent: azdata.FormComponent; private targetTextBox: azdata.InputBoxComponent; private targetFileButton: azdata.ButtonComponent; + private targetStructureDropdown: azdata.DropDownComponent; private targetServerComponent: azdata.FormComponent; protected targetServerDropdown: azdata.DropDownComponent; private targetConnectionButton: azdata.ButtonComponent; private targetDatabaseComponent: azdata.FormComponent; private targetDatabaseDropdown: azdata.DropDownComponent; - private formBuilder: azdata.FormBuilder; - private sourceIsDacpac: boolean; - private targetIsDacpac: boolean; - private connectionId: string; - private sourceDbEditable: string; + private targetDacpacPath: string; + private targetProjectFilePath: string; + private targetEndpointType: mssql.SchemaCompareEndpointType; private targetDbEditable: string; private previousSource: mssql.SchemaCompareEndpointInfo; private previousTarget: mssql.SchemaCompareEndpointInfo; + private formBuilder: azdata.FormBuilder; + private connectionId: string; + private toDispose: vscode.Disposable[] = []; private initDialogComplete: Deferred; private initDialogPromise: Promise = new Promise((resolve, reject) => this.initDialogComplete = { resolve, reject }); @@ -59,6 +70,11 @@ export class SchemaCompareDialog { constructor(private schemaCompareMainWindow: SchemaCompareMainWindow, private view?: azdata.ModelView, private extensionContext?: vscode.ExtensionContext) { this.previousSource = schemaCompareMainWindow.sourceEndpointInfo; this.previousTarget = schemaCompareMainWindow.targetEndpointInfo; + + this.dialog = azdata.window.createModelViewDialog(loc.SchemaCompareLabel); + this.dialog.registerCloseValidator(async () => { + return this.validate(); + }); } protected async initializeDialog(): Promise { @@ -79,31 +95,17 @@ export class SchemaCompareDialog { this.dialog.okButton.label = loc.OkButtonText; this.dialog.okButton.enabled = false; - this.dialog.okButton.onClick(async () => await this.execute()); + this.toDispose.push(this.dialog.okButton.onClick(async () => await this.handleOkButtonClick())); this.dialog.cancelButton.label = loc.CancelButtonText; - this.dialog.cancelButton.onClick(async () => await this.cancel()); + this.toDispose.push(this.dialog.cancelButton.onClick(async () => await this.cancel())); azdata.window.openDialog(this.dialog); await this.initDialogPromise; } public async execute(): Promise { - if (this.sourceIsDacpac) { - this.schemaCompareMainWindow.sourceEndpointInfo = { - endpointType: mssql.SchemaCompareEndpointType.Dacpac, - serverDisplayName: '', - serverName: '', - databaseName: '', - ownerUri: '', - packageFilePath: this.sourceTextBox.value, - connectionDetails: undefined, - projectFilePath: '', - folderStructure: '', - targetScripts: [], - dataSchemaProvider: '' - }; - } else { + if (this.sourceEndpointType === mssql.SchemaCompareEndpointType.Database) { const sourceServerDropdownValue = this.sourceServerDropdown.value as ConnectionDropdownValue; const ownerUri = await azdata.connection.getUriForConnection(sourceServerDropdownValue.connection.connectionId); @@ -113,31 +115,45 @@ export class SchemaCompareDialog { serverName: sourceServerDropdownValue.name, databaseName: this.sourceDatabaseDropdown.value.toString(), ownerUri: ownerUri, - packageFilePath: '', - connectionDetails: undefined, - connectionName: sourceServerDropdownValue.connection.options.connectionName, projectFilePath: '', - folderStructure: '', targetScripts: [], - dataSchemaProvider: '' + folderStructure: '', + packageFilePath: '', + dataSchemaProvider: '', + connectionDetails: undefined, + connectionName: sourceServerDropdownValue.connection.options.connectionName }; - } - - if (this.targetIsDacpac) { - this.schemaCompareMainWindow.targetEndpointInfo = { + } else if (this.sourceEndpointType === mssql.SchemaCompareEndpointType.Dacpac) { + this.schemaCompareMainWindow.sourceEndpointInfo = { endpointType: mssql.SchemaCompareEndpointType.Dacpac, serverDisplayName: '', serverName: '', databaseName: '', ownerUri: '', - packageFilePath: this.targetTextBox.value, - connectionDetails: undefined, projectFilePath: '', - folderStructure: '', targetScripts: [], - dataSchemaProvider: '' + folderStructure: '', + dataSchemaProvider: '', + packageFilePath: this.sourceTextBox.value, + connectionDetails: undefined }; } else { + this.schemaCompareMainWindow.sourceEndpointInfo = { + endpointType: mssql.SchemaCompareEndpointType.Project, + projectFilePath: this.sourceTextBox.value, + targetScripts: await this.getTargetScripts(true), + dataSchemaProvider: await this.getDsp(this.sourceTextBox.value), + folderStructure: '', + serverDisplayName: '', + serverName: '', + databaseName: '', + ownerUri: '', + packageFilePath: '', + connectionDetails: undefined + }; + } + + if (this.targetEndpointType === mssql.SchemaCompareEndpointType.Database) { const targetServerDropdownValue = this.targetServerDropdown.value as ConnectionDropdownValue; const ownerUri = await azdata.connection.getUriForConnection(targetServerDropdownValue.connection.connectionId); @@ -147,20 +163,48 @@ export class SchemaCompareDialog { serverName: targetServerDropdownValue.name, databaseName: this.targetDatabaseDropdown.value.toString(), ownerUri: ownerUri, - packageFilePath: '', - connectionDetails: undefined, - connectionName: targetServerDropdownValue.connection.options.connectionName, projectFilePath: '', folderStructure: '', targetScripts: [], - dataSchemaProvider: '' + packageFilePath: '', + dataSchemaProvider: '', + connectionDetails: undefined, + connectionName: targetServerDropdownValue.connection.options.connectionName + }; + } else if (this.targetEndpointType === mssql.SchemaCompareEndpointType.Dacpac) { + this.schemaCompareMainWindow.targetEndpointInfo = { + endpointType: mssql.SchemaCompareEndpointType.Dacpac, + serverDisplayName: '', + serverName: '', + databaseName: '', + ownerUri: '', + projectFilePath: '', + folderStructure: '', + targetScripts: [], + dataSchemaProvider: '', + packageFilePath: this.targetTextBox.value, + connectionDetails: undefined + }; + } else { + this.schemaCompareMainWindow.targetEndpointInfo = { + endpointType: mssql.SchemaCompareEndpointType.Project, + projectFilePath: this.targetTextBox.value, + folderStructure: this.targetStructureDropdown!.value as string, + targetScripts: await this.getTargetScripts(false), + dataSchemaProvider: await this.getDsp(this.targetTextBox.value), + serverDisplayName: '', + serverName: '', + databaseName: '', + ownerUri: '', + packageFilePath: '', + connectionDetails: undefined }; } TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareDialog, 'SchemaCompareStart') .withAdditionalProperties({ - sourceIsDacpac: this.sourceIsDacpac.toString(), - targetIsDacpac: this.targetIsDacpac.toString() + sourceEndpointType: getSchemaCompareEndpointString(this.sourceEndpointType), + targetEndpointType: getSchemaCompareEndpointString(this.targetEndpointType) }).send(); // update source and target values that are displayed @@ -198,6 +242,7 @@ export class SchemaCompareDialog { } protected async cancel(): Promise { + this.dispose(); } private async initializeSchemaCompareTab(): Promise { @@ -206,36 +251,67 @@ export class SchemaCompareDialog { this.view = view; } + let sourceValue = ''; + + if (this.schemaCompareMainWindow.sourceEndpointInfo && this.schemaCompareMainWindow.sourceEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Dacpac) { + sourceValue = this.schemaCompareMainWindow.sourceEndpointInfo.packageFilePath; + } else if (this.schemaCompareMainWindow.sourceEndpointInfo && this.schemaCompareMainWindow.sourceEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Project) { + sourceValue = this.schemaCompareMainWindow.sourceEndpointInfo.projectFilePath; + } + this.sourceTextBox = this.view.modelBuilder.inputBox().withProps({ - value: this.schemaCompareMainWindow.sourceEndpointInfo ? this.schemaCompareMainWindow.sourceEndpointInfo.packageFilePath : '', + value: sourceValue, width: this.textBoxWidth, ariaLabel: loc.sourceFile }).component(); this.sourceTextBox.onTextChanged(async (e) => { this.dialog.okButton.enabled = await this.shouldEnableOkayButton(); + + if (this.sourceEndpointType === mssql.SchemaCompareEndpointType.Dacpac) { + this.sourceDacpacPath = e; + } else if (this.sourceEndpointType === mssql.SchemaCompareEndpointType.Project) { + this.sourceProjectFilePath = e; + } }); + let targetValue = ''; + + if (this.schemaCompareMainWindow.targetEndpointInfo && this.schemaCompareMainWindow.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Dacpac) { + targetValue = this.schemaCompareMainWindow.targetEndpointInfo.packageFilePath; + } else if (this.schemaCompareMainWindow.targetEndpointInfo && this.schemaCompareMainWindow.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Project) { + targetValue = this.schemaCompareMainWindow.targetEndpointInfo.projectFilePath; + } + this.targetTextBox = this.view.modelBuilder.inputBox().withProps({ - value: this.schemaCompareMainWindow.targetEndpointInfo ? this.schemaCompareMainWindow.targetEndpointInfo.packageFilePath : '', + value: targetValue, width: this.textBoxWidth, ariaLabel: loc.targetFile }).component(); - this.targetTextBox.onTextChanged(async () => { + this.targetTextBox.onTextChanged(async (e) => { this.dialog.okButton.enabled = await this.shouldEnableOkayButton(); + + if (this.targetEndpointType === mssql.SchemaCompareEndpointType.Dacpac) { + this.targetDacpacPath = e; + } else if (this.targetEndpointType === mssql.SchemaCompareEndpointType.Project) { + this.targetProjectFilePath = e; + } }); this.sourceServerComponent = this.createSourceServerDropdown(); - this.sourceDatabaseComponent = this.createSourceDatabaseDropdown(); this.targetServerComponent = this.createTargetServerDropdown(); - this.targetDatabaseComponent = this.createTargetDatabaseDropdown(); - this.sourceDacpacComponent = this.createFileBrowser(false, this.schemaCompareMainWindow.sourceEndpointInfo); - this.targetDacpacComponent = this.createFileBrowser(true, this.schemaCompareMainWindow.targetEndpointInfo); + this.sourceDacpacComponent = this.createFileBrowser(false, true, this.schemaCompareMainWindow.sourceEndpointInfo); + this.targetDacpacComponent = this.createFileBrowser(true, true, this.schemaCompareMainWindow.targetEndpointInfo); + + this.sourceProjectFilePathComponent = this.createFileBrowser(false, false, this.schemaCompareMainWindow.sourceEndpointInfo); + this.targetProjectFilePathComponent = this.createFileBrowser(true, false, this.schemaCompareMainWindow.targetEndpointInfo); + + this.targetProjectStructureComponent = this.createStructureDropdown(); let sourceRadioButtons = this.createSourceRadioButtons(); let targetRadioButtons = this.createTargetRadioButtons(); @@ -243,18 +319,27 @@ export class SchemaCompareDialog { let sourceComponents = []; let targetComponents = []; - // start source and target with either dacpac or database selection based on what the previous value was + // start source and target with either dacpac, database, or project selection based on what the previous value was if (this.schemaCompareMainWindow.sourceEndpointInfo && this.schemaCompareMainWindow.sourceEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Database) { sourceComponents = [ sourceRadioButtons, this.sourceServerComponent, this.sourceDatabaseComponent ]; - } else { + } else if (this.schemaCompareMainWindow.sourceEndpointInfo && this.schemaCompareMainWindow.sourceEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Dacpac) { sourceComponents = [ sourceRadioButtons, this.sourceDacpacComponent, ]; + } else if (this.schemaCompareMainWindow.sourceEndpointInfo && this.schemaCompareMainWindow.sourceEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Project) { + sourceComponents = [ + sourceRadioButtons, + this.sourceProjectFilePathComponent, + ]; + } else { + sourceComponents = [ + sourceRadioButtons, + ]; } if (this.schemaCompareMainWindow.targetEndpointInfo && this.schemaCompareMainWindow.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Database) { @@ -263,11 +348,21 @@ export class SchemaCompareDialog { this.targetServerComponent, this.targetDatabaseComponent ]; - } else { + } else if (this.schemaCompareMainWindow.targetEndpointInfo && this.schemaCompareMainWindow.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Dacpac) { targetComponents = [ targetRadioButtons, this.targetDacpacComponent, ]; + } else if (this.schemaCompareMainWindow.targetEndpointInfo && this.schemaCompareMainWindow.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Project) { + targetComponents = [ + targetRadioButtons, + this.targetProjectFilePathComponent, + this.targetProjectStructureComponent + ]; + } else { + targetComponents = [ + targetRadioButtons, + ]; } this.formBuilder = this.view.modelBuilder.formContainer() @@ -290,17 +385,26 @@ export class SchemaCompareDialog { let formModel = this.formBuilder.component(); await this.view.initializeModel(formModel); - if (this.sourceIsDacpac) { - await this.sourceDacpacRadioButton.focus(); - } else { - await this.sourceDatabaseRadioButton.focus(); + + switch (this.sourceEndpointType) { + case (mssql.SchemaCompareEndpointType.Database): + await this.sourceDatabaseRadioButton.focus(); + break; + case (mssql.SchemaCompareEndpointType.Dacpac): + await this.sourceDacpacRadioButton.focus(); + break; + case (mssql.SchemaCompareEndpointType.Project): + await this.sourceProjectRadioButton.focus(); + break; } + this.initDialogComplete.resolve(); }); } - private createFileBrowser(isTarget: boolean, endpoint: mssql.SchemaCompareEndpointInfo): azdata.FormComponent { + private createFileBrowser(isTarget: boolean, dacpac: boolean, endpoint: mssql.SchemaCompareEndpointInfo): azdata.FormComponent { let currentTextbox = isTarget ? this.targetTextBox : this.sourceTextBox; + if (isTarget) { this.targetFileButton = this.view.modelBuilder.button().withProps({ title: loc.selectTargetFile, @@ -318,8 +422,9 @@ export class SchemaCompareDialog { } let currentButton = isTarget ? this.targetFileButton : this.sourceFileButton; + const filter = dacpac ? 'dacpac' : 'sqlproj'; - currentButton.onDidClick(async (click) => { + currentButton.onDidClick(async () => { // file browser should open where the current dacpac is or the appropriate default folder let rootPath = getRootPath(); let defaultUri = endpoint && endpoint.packageFilePath && await exists(endpoint.packageFilePath) ? endpoint.packageFilePath : rootPath; @@ -332,7 +437,7 @@ export class SchemaCompareDialog { defaultUri: vscode.Uri.file(defaultUri), openLabel: loc.open, filters: { - 'dacpac Files': ['dacpac'], + 'Files': [filter], } } ); @@ -352,6 +457,22 @@ export class SchemaCompareDialog { }; } + private createStructureDropdown(): azdata.FormComponent { + this.targetStructureDropdown = this.view.modelBuilder.dropDown().withProps({ + editable: true, + fireOnTextChange: true, + ariaLabel: loc.targetStructure, + width: this.textBoxWidth, + values: [loc.file, loc.flat, loc.objectType, loc.schema, loc.schemaObjectType], + value: loc.schemaObjectType, + }).component(); + + return { + component: this.targetStructureDropdown, + title: loc.StructureDropdownLabel, + }; + } + private createSourceRadioButtons(): azdata.FormComponent { this.sourceDacpacRadioButton = this.view.modelBuilder.radioButton() .withProps({ @@ -365,36 +486,66 @@ export class SchemaCompareDialog { label: loc.DatabaseRadioButtonLabel }).component(); + this.sourceProjectRadioButton = this.view.modelBuilder.radioButton() + .withProps({ + name: 'source', + label: loc.ProjectRadioButtonLabel + }).component(); + // show dacpac file browser this.sourceDacpacRadioButton.onDidClick(async () => { - this.sourceIsDacpac = true; + this.sourceEndpointType = mssql.SchemaCompareEndpointType.Dacpac; + this.sourceTextBox.value = this.sourceDacpacPath; this.formBuilder.removeFormItem(this.sourceServerComponent); this.formBuilder.removeFormItem(this.sourceDatabaseComponent); + this.formBuilder.removeFormItem(this.sourceProjectFilePathComponent); this.formBuilder.insertFormItem(this.sourceDacpacComponent, 2, { horizontal: true, titleFontSize: titleFontSize }); this.dialog.okButton.enabled = await this.shouldEnableOkayButton(); }); // show server and db dropdowns this.sourceDatabaseRadioButton.onDidClick(async () => { - this.sourceIsDacpac = false; + this.sourceEndpointType = mssql.SchemaCompareEndpointType.Database; this.formBuilder.insertFormItem(this.sourceServerComponent, 2, { horizontal: true, titleFontSize: titleFontSize }); this.formBuilder.insertFormItem(this.sourceDatabaseComponent, 3, { horizontal: true, titleFontSize: titleFontSize }); this.formBuilder.removeFormItem(this.sourceDacpacComponent); + this.formBuilder.removeFormItem(this.sourceProjectFilePathComponent); await this.populateServerDropdown(false); }); + // show project directory browser + this.sourceProjectRadioButton.onDidClick(async () => { + this.sourceEndpointType = mssql.SchemaCompareEndpointType.Project; + this.sourceTextBox.value = this.sourceProjectFilePath; + this.formBuilder.removeFormItem(this.sourceServerComponent); + this.formBuilder.removeFormItem(this.sourceDatabaseComponent); + this.formBuilder.removeFormItem(this.sourceDacpacComponent); + this.formBuilder.insertFormItem(this.sourceProjectFilePathComponent, 2, { horizontal: true, titleFontSize: titleFontSize }); + this.dialog.okButton.enabled = await this.shouldEnableOkayButton(); + }); + // if source is currently a db, show it in the server and db dropdowns if (this.schemaCompareMainWindow.sourceEndpointInfo && this.schemaCompareMainWindow.sourceEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Database) { this.sourceDatabaseRadioButton.checked = true; - this.sourceIsDacpac = false; - } else { + this.sourceEndpointType = mssql.SchemaCompareEndpointType.Database; + } else if (this.schemaCompareMainWindow.sourceEndpointInfo && this.schemaCompareMainWindow.sourceEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Dacpac) { this.sourceDacpacRadioButton.checked = true; - this.sourceIsDacpac = true; + this.sourceEndpointType = mssql.SchemaCompareEndpointType.Dacpac; + } else if (this.schemaCompareMainWindow.sourceEndpointInfo) { + this.sourceProjectRadioButton.checked = true; + this.sourceEndpointType = mssql.SchemaCompareEndpointType.Project; } + + let radioButtons = [this.sourceDatabaseRadioButton, this.sourceDacpacRadioButton]; + + if (vscode.extensions.getExtension(loc.sqlDatabaseProjectExtensionId)) { + radioButtons.push(this.sourceProjectRadioButton); + } + let flexRadioButtonsModel = this.view.modelBuilder.flexContainer() .withLayout({ flexFlow: 'column' }) - .withItems([this.sourceDacpacRadioButton, this.sourceDatabaseRadioButton]) + .withItems(radioButtons) .withProps({ ariaRole: 'radiogroup' }) .component(); @@ -417,38 +568,69 @@ export class SchemaCompareDialog { label: loc.DatabaseRadioButtonLabel }).component(); + let projectRadioButton = this.view.modelBuilder.radioButton() + .withProps({ + name: 'target', + label: loc.ProjectRadioButtonLabel + }).component(); + // show dacpac file browser dacpacRadioButton.onDidClick(async () => { - this.targetIsDacpac = true; + this.targetEndpointType = mssql.SchemaCompareEndpointType.Dacpac; + this.targetTextBox.value = this.targetDacpacPath; this.formBuilder.removeFormItem(this.targetServerComponent); this.formBuilder.removeFormItem(this.targetDatabaseComponent); + this.formBuilder.removeFormItem(this.targetProjectFilePathComponent); + this.formBuilder.removeFormItem(this.targetProjectStructureComponent); this.formBuilder.addFormItem(this.targetDacpacComponent, { horizontal: true, titleFontSize: titleFontSize }); this.dialog.okButton.enabled = await this.shouldEnableOkayButton(); }); // show server and db dropdowns databaseRadioButton.onDidClick(async () => { - this.targetIsDacpac = false; + this.targetEndpointType = mssql.SchemaCompareEndpointType.Database; this.formBuilder.removeFormItem(this.targetDacpacComponent); + this.formBuilder.removeFormItem(this.targetProjectFilePathComponent); + this.formBuilder.removeFormItem(this.targetProjectStructureComponent); this.formBuilder.addFormItem(this.targetServerComponent, { horizontal: true, titleFontSize: titleFontSize }); this.formBuilder.addFormItem(this.targetDatabaseComponent, { horizontal: true, titleFontSize: titleFontSize }); await this.populateServerDropdown(true); }); + // show project directory browser + projectRadioButton.onDidClick(async () => { + this.targetEndpointType = mssql.SchemaCompareEndpointType.Project; + this.targetTextBox.value = this.targetProjectFilePath; + this.formBuilder.removeFormItem(this.targetServerComponent); + this.formBuilder.removeFormItem(this.targetDatabaseComponent); + this.formBuilder.removeFormItem(this.targetDacpacComponent); + this.formBuilder.addFormItem(this.targetProjectFilePathComponent, { horizontal: true, titleFontSize: titleFontSize }); + this.formBuilder.addFormItem(this.targetProjectStructureComponent, { horizontal: true, titleFontSize: titleFontSize }); + this.dialog.okButton.enabled = await this.shouldEnableOkayButton(); + }); + // if target is currently a db, show it in the server and db dropdowns if (this.schemaCompareMainWindow.targetEndpointInfo && this.schemaCompareMainWindow.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Database) { databaseRadioButton.checked = true; - this.targetIsDacpac = false; - } else { + this.targetEndpointType = mssql.SchemaCompareEndpointType.Database; + } else if (this.schemaCompareMainWindow.targetEndpointInfo && this.schemaCompareMainWindow.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Dacpac) { dacpacRadioButton.checked = true; - this.targetIsDacpac = true; + this.targetEndpointType = mssql.SchemaCompareEndpointType.Dacpac; + } else if (this.schemaCompareMainWindow.targetEndpointInfo) { + projectRadioButton.checked = true; + this.targetEndpointType = mssql.SchemaCompareEndpointType.Project; + } + + let radioButtons = [databaseRadioButton, dacpacRadioButton]; + + if (vscode.extensions.getExtension(loc.sqlDatabaseProjectExtensionId)) { + radioButtons.push(projectRadioButton); } let flexRadioButtonsModel = this.view.modelBuilder.flexContainer() .withLayout({ flexFlow: 'column' }) - .withItems([dacpacRadioButton, databaseRadioButton] - ) + .withItems(radioButtons) .withProps({ ariaRole: 'radiogroup' }) .component(); @@ -459,18 +641,83 @@ export class SchemaCompareDialog { } private async shouldEnableOkayButton(): Promise { - let sourcefilled = (this.sourceIsDacpac && await this.existsDacpac(this.sourceTextBox.value)) - || (!this.sourceIsDacpac && !isNullOrUndefined(this.sourceDatabaseDropdown.value) && this.sourceDatabaseDropdown.values.findIndex(x => this.matchesValue(x, this.sourceDbEditable)) !== -1); - let targetfilled = (this.targetIsDacpac && await this.existsDacpac(this.targetTextBox.value)) - || (!this.targetIsDacpac && !isNullOrUndefined(this.targetDatabaseDropdown.value) && this.targetDatabaseDropdown.values.findIndex(x => this.matchesValue(x, this.targetDbEditable)) !== -1); + let sourcefilled = (this.sourceEndpointType === mssql.SchemaCompareEndpointType.Dacpac && await this.existsDacpac(this.sourceTextBox.value)) + || (this.sourceEndpointType === mssql.SchemaCompareEndpointType.Project && this.existsProjectFile(this.sourceTextBox.value)) + || (this.sourceEndpointType === mssql.SchemaCompareEndpointType.Database && !isNullOrUndefined(this.sourceDatabaseDropdown.value) && this.sourceDatabaseDropdown.values.findIndex(x => this.matchesValue(x, this.sourceDbEditable)) !== -1); + let targetfilled = (this.targetEndpointType === mssql.SchemaCompareEndpointType.Dacpac && await this.existsDacpac(this.targetTextBox.value)) + || (this.targetEndpointType === mssql.SchemaCompareEndpointType.Project && this.existsProjectFile(this.targetTextBox.value)) + || (this.targetEndpointType === mssql.SchemaCompareEndpointType.Database && !isNullOrUndefined(this.targetDatabaseDropdown.value) && this.targetDatabaseDropdown.values.findIndex(x => this.matchesValue(x, this.targetDbEditable)) !== -1); return sourcefilled && targetfilled; } + public async handleOkButtonClick(): Promise { + await this.execute(); + this.dispose(); + } + + protected showErrorMessage(message: string): void { + this.dialog.message = { + text: message, + level: getAzdataApi()!.window.MessageLevel.Error + }; + } + + async validate(): Promise { + try { + // check project extension is installed + if (!vscode.extensions.getExtension(loc.sqlDatabaseProjectExtensionId) && + (this.sourceEndpointType === mssql.SchemaCompareEndpointType.Project || + this.targetEndpointType === mssql.SchemaCompareEndpointType.Project)) { + this.showErrorMessage(loc.noProjectExtension); + return false; + } + + // check Database Schema Providers are set and valid + if (this.sourceEndpointType === mssql.SchemaCompareEndpointType.Project) { + try { + await this.getDsp(this.sourceTextBox.value); + } catch (err) { + this.showErrorMessage(loc.dspErrorSource); + } + } + + if (this.targetEndpointType === mssql.SchemaCompareEndpointType.Project) { + try { + await this.getDsp(this.targetTextBox.value); + } catch (err) { + this.showErrorMessage(loc.dspErrorTarget); + } + } + + return true; + } catch (e) { + this.showErrorMessage(e?.message ? e.message : e); + return false; + } + } + + private dispose(): void { + this.toDispose.forEach(disposable => disposable.dispose()); + } + private async existsDacpac(filename: string): Promise { return !isNullOrUndefined(filename) && await exists(filename) && (filename.toLocaleLowerCase().endsWith('.dacpac')); } + private async existsProjectFile(filename: string): Promise { + return !isNullOrUndefined(filename) && await exists(filename) && (filename.toLocaleLowerCase().endsWith('.sqlproj')); + } + + private async getTargetScripts(source: boolean): Promise { + const projectFilePath = source ? this.sourceTextBox.value : this.targetTextBox.value; + return await vscode.commands.executeCommand(loc.sqlDatabaseProjectsGetTargetScripts, projectFilePath); + } + + private async getDsp(projectFilePath: string): Promise { + return await vscode.commands.executeCommand(loc.sqlDatabaseProjectsGetDsp, projectFilePath); + } + protected createSourceServerDropdown(): azdata.FormComponent { this.sourceServerDropdown = this.view.modelBuilder.dropDown().withProps( { @@ -540,9 +787,7 @@ export class SchemaCompareDialog { width: this.textBoxWidth } ).component(); - this.targetConnectionButton = this.createConnectionButton(true); - this.targetServerDropdown.onValueChanged(async (value) => { if (value.selected && this.targetServerDropdown.values.findIndex(x => this.matchesValue(x, value.selected)) === -1) { await this.targetDatabaseDropdown.updateProperties({ @@ -555,10 +800,8 @@ export class SchemaCompareDialog { await this.populateDatabaseDropdown((this.targetServerDropdown.value as ConnectionDropdownValue).connection, true); } }); - // don't await so that dialog loading won't be blocked. Dropdown will show loading indicator until it is populated this.populateServerDropdown(true); - return { component: this.targetServerDropdown, title: loc.ServerDropdownLabel, diff --git a/extensions/schema-compare/src/extension.ts b/extensions/schema-compare/src/extension.ts index ce2fa777d0..2f26ed4800 100644 --- a/extensions/schema-compare/src/extension.ts +++ b/extensions/schema-compare/src/extension.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode'; import { SchemaCompareMainWindow } from './schemaCompareMainWindow'; export async function activate(extensionContext: vscode.ExtensionContext): Promise { - vscode.commands.registerCommand('schemaCompare.start', async (context: any) => { await new SchemaCompareMainWindow(undefined, extensionContext, undefined).start(context); }); + vscode.commands.registerCommand('schemaCompare.start', async (sourceContext: any, targetContext: any = undefined, comparisonResult: any = undefined) => { await new SchemaCompareMainWindow(undefined, extensionContext, undefined).start(sourceContext, targetContext, comparisonResult); }); } export function deactivate(): void { diff --git a/extensions/schema-compare/src/localizedConstants.ts b/extensions/schema-compare/src/localizedConstants.ts index a00c8ce4fe..2f483bd24a 100644 --- a/extensions/schema-compare/src/localizedConstants.ts +++ b/extensions/schema-compare/src/localizedConstants.ts @@ -14,9 +14,11 @@ export const TargetTitle: string = localize('schemaCompareDialog.TargetTitle', " export const FileTextBoxLabel: string = localize('schemaCompareDialog.fileTextBoxLabel', "File"); export const DacpacRadioButtonLabel: string = localize('schemaCompare.dacpacRadioButtonLabel', "Data-tier Application File (.dacpac)"); export const DatabaseRadioButtonLabel: string = localize('schemaCompare.databaseButtonLabel', "Database"); +export const ProjectRadioButtonLabel: string = localize('schemaCompare.projectButtonLabel', "Database Project"); export const RadioButtonsLabel: string = localize('schemaCompare.radioButtonsLabel', "Type"); export const ServerDropdownLabel: string = localize('schemaCompareDialog.serverDropdownTitle', "Server"); export const DatabaseDropdownLabel: string = localize('schemaCompareDialog.databaseDropdownTitle', "Database"); +export const StructureDropdownLabel: string = localize('schemaCompareDialog.structureDropdownLabel', "Folder Structure"); export const SchemaCompareLabel: string = localize('schemaCompare.dialogTitle', "Schema Compare"); export const differentSourceMessage: string = localize('schemaCompareDialog.differentSourceMessage', "A different source schema has been selected. Compare to see the comparison?"); export const differentTargetMessage: string = localize('schemaCompareDialog.differentTargetMessage', "A different target schema has been selected. Compare to see the comparison?"); @@ -31,6 +33,12 @@ export const sourceServer: string = localize('schemaCompareDialog.sourceServerDr export const targetServer: string = localize('schemaCompareDialog.targetServerDropdown', "Target Server"); export const defaultText: string = localize('schemaCompareDialog.defaultUser', "default"); export const open: string = localize('schemaCompare.openFile', "Open"); +export const targetStructure = localize('targetStructure', "Target Folder Structure"); +export const file = localize('file', "File"); +export const flat = localize('flat', "Flat"); +export const objectType = localize('objectType', "Object Type"); +export const schema = localize('schema', "Schema"); +export const schemaObjectType = localize('schemaObjectType', "Schema/Object Type"); export const selectSourceFile: string = localize('schemaCompare.selectSourceFile', "Select source file"); export const selectTargetFile: string = localize('schemaCompare.selectTargetFile', "Select target file"); export const ResetButtonText: string = localize('SchemaCompareOptionsDialog.Reset', "Reset"); @@ -61,7 +69,7 @@ export const include: string = localize('schemaCompare.includeColumnName', "Incl export const action: string = localize('schemaCompare.actionColumn', "Action"); export const targetName: string = localize('schemaCompare.targetNameColumn', "Target Name"); export const generateScriptDisabled: string = localize('schemaCompare.generateScriptButtonDisabledTitle', "Generate script is enabled when the target is a database"); -export const applyDisabled: string = localize('schemaCompare.applyButtonDisabledTitle', "Apply is enabled when the target is a database"); +export const applyDisabled: string = localize('schemaCompare.applyButtonDisabledTitle', "Apply is enabled when the target is a database or database project"); export function cannotExcludeMessageDependent(diffEntryName: string, firstDependentName: string): string { return localize('schemaCompare.cannotExcludeMessageWithDependent', "Cannot exclude {0}. Included dependents exist, such as {1}", diffEntryName, firstDependentName); } export function cannotIncludeMessageDependent(diffEntryName: string, firstDependentName: string): string { return localize('schemaCompare.cannotIncludeMessageWithDependent', "Cannot include {0}. Excluded dependents exist, such as {1}", diffEntryName, firstDependentName); } export function cannotExcludeMessage(diffEntryName: string): string { return localize('schemaCompare.cannotExcludeMessage', "Cannot exclude {0}. Included dependents exist", diffEntryName); } @@ -318,3 +326,20 @@ export function cancelErrorMessage(errorMessage: string): string { return locali export function generateScriptErrorMessage(errorMessage: string): string { return localize('schemaCompare.generateScriptErrorMessage', "Generate script failed: '{0}'", (errorMessage) ? errorMessage : 'Unknown'); } export function applyErrorMessage(errorMessage: string): string { return localize('schemaCompare.updateErrorMessage', "Schema Compare Apply failed '{0}'", errorMessage ? errorMessage : 'Unknown'); } export function openScmpErrorMessage(errorMessage: string): string { return localize('schemaCompare.openScmpErrorMessage', "Open scmp failed: '{0}'", (errorMessage) ? errorMessage : 'Unknown'); } +export const applyError: string = localize('schemaCompare.applyError', "There was an error updating the project"); +export const dspErrorSource: string = localize('schemaCompareDialog.dspErrorSource', "The source .sqlproj file does not specify a database schema component"); +export const dspErrorTarget: string = localize('schemaCompareDialog.dspErrorTarget', "The target .sqlproj file does not specify a database schema component"); +export const noProjectExtension: string = localize('schemaCompareDialog.noProjectExtension', "The sql-database-projects extension is required to perform schema comparison with database projects"); +export const noProjectExtensionApply: string = localize('schemaCompareDialog.noProjectExtensionApply', "The sql-database-projects extension is required to apply changes to a project"); + +// Information messages +export const applySuccess: string = localize('schemaCompare.applySuccess', "Project was successfully updated"); + +// Extensions +export const sqlDatabaseProjectExtensionId: string = 'microsoft.sql-database-projects'; + +// Commands +export const sqlDatabaseProjectsGetTargetScripts: string = 'sqlDatabaseProjects.schemaCompareGetTargetScripts'; +export const sqlDatabaseProjectsGetDsp: string = 'sqlDatabaseProjects.schemaCompareGetDsp'; +export const sqlDatabaseProjectsPublishChanges: string = 'sqlDatabaseProjects.schemaComparePublishProjectChanges'; +export const sqlDatabaseProjectsShowProjectsView: string = 'sqlDatabaseProjects.schemaCompareShowProjectsView'; diff --git a/extensions/schema-compare/src/schemaCompareMainWindow.ts b/extensions/schema-compare/src/schemaCompareMainWindow.ts index 9abe6ff64f..270732f692 100644 --- a/extensions/schema-compare/src/schemaCompareMainWindow.ts +++ b/extensions/schema-compare/src/schemaCompareMainWindow.ts @@ -11,7 +11,7 @@ import * as mssql from '../../mssql'; import * as loc from './localizedConstants'; import { SchemaCompareOptionsDialog } from './dialogs/schemaCompareOptionsDialog'; import { TelemetryReporter, TelemetryViews } from './telemetry'; -import { getTelemetryErrorType, getEndpointName, verifyConnectionAndGetOwnerUri, getRootPath } from './utils'; +import { getTelemetryErrorType, getEndpointName, verifyConnectionAndGetOwnerUri, getRootPath, getSchemaCompareEndpointString } from './utils'; import { SchemaCompareDialog } from './dialogs/schemaCompareDialog'; import { isNullOrUndefined } from 'util'; @@ -81,14 +81,33 @@ export class SchemaCompareMainWindow { this.editor = azdata.workspace.createModelViewEditor(loc.SchemaCompareLabel, { retainContextWhenHidden: true, supportsSave: true, resourceName: schemaCompareResourceName }, 'SchemaCompareEditor'); } - // schema compare can get started with three contexts for the source: + // schema compare can get started with four contexts for the source: // 1. undefined // 2. connection profile // 3. dacpac - public async start(context: any): Promise { - // if schema compare was launched from a db, set that as the source - let profile = context ? context.connectionProfile : undefined; - let sourceDacpac = context as string; + // 4. project + public async start(sourceContext: any, targetContext: mssql.SchemaCompareEndpointInfo = undefined, comparisonResult: mssql.SchemaCompareResult = undefined): Promise { + const targetIsSetAsProject: boolean = targetContext && targetContext.endpointType === mssql.SchemaCompareEndpointType.Project; + + // if schema compare was launched from a db or a connection profile, set that as the source + let profile: azdata.IConnectionProfile; + + if (targetIsSetAsProject) { + profile = sourceContext; + this.targetEndpointInfo = targetContext; + } else { + profile = sourceContext ? sourceContext.connectionProfile : undefined; + } + + let sourceDacpac = undefined; + let sourceProject = undefined; + + if (!profile && sourceContext as string && (sourceContext as string).endsWith('.dacpac')) { + sourceDacpac = sourceContext as string; + } else if (!profile) { + sourceProject = sourceContext as string; + } + if (profile) { let ownerUri = await azdata.connection.getUriForConnection((profile.id)); let usr = profile.userName; @@ -106,9 +125,9 @@ export class SchemaCompareMainWindow { connectionDetails: undefined, connectionName: profile.connectionName, projectFilePath: '', - folderStructure: '', targetScripts: [], - dataSchemaProvider: '' + dataSchemaProvider: '', + folderStructure: '' }; } else if (sourceDacpac) { this.sourceEndpointInfo = { @@ -120,9 +139,23 @@ export class SchemaCompareMainWindow { packageFilePath: sourceDacpac, connectionDetails: undefined, projectFilePath: '', - folderStructure: '', targetScripts: [], - dataSchemaProvider: '' + dataSchemaProvider: '', + folderStructure: '' + }; + } else if (sourceProject) { + this.sourceEndpointInfo = { + endpointType: mssql.SchemaCompareEndpointType.Project, + packageFilePath: '', + serverDisplayName: '', + serverName: '', + databaseName: '', + ownerUri: '', + connectionDetails: undefined, + projectFilePath: sourceProject, + targetScripts: [], + dataSchemaProvider: undefined, + folderStructure: '' }; } @@ -131,6 +164,10 @@ export class SchemaCompareMainWindow { this.registerContent(), this.editor.openEditor() ]); + + if (targetIsSetAsProject) { + await this.execute(comparisonResult); + } } private async registerContent(): Promise { @@ -170,7 +207,8 @@ export class SchemaCompareMainWindow { this.createSourceAndTargetButtons(); this.sourceName = getEndpointName(this.sourceEndpointInfo); - this.targetName = ' '; + this.targetName = getEndpointName(this.targetEndpointInfo); + this.sourceNameComponent = this.view.modelBuilder.inputBox().withProps({ value: this.sourceName, title: this.sourceName, @@ -283,33 +321,43 @@ export class SchemaCompareMainWindow { this.deploymentOptions = deploymentOptions; } - public async execute() { - TelemetryReporter.sendActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaComparisonStarted'); + public async execute(comparisonResult: mssql.SchemaCompareCompletionResult = undefined) { const service = await this.getService(); - if (!this.operationId) { - // create once per page - this.operationId = generateGuid(); - } + if (comparisonResult) { + this.operationId = comparisonResult.operationId; + this.comparisonResult = comparisonResult; + this.flexModel.removeItem(this.startText); + } else { + TelemetryReporter.sendActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaComparisonStarted'); - this.comparisonResult = await service.schemaCompare(this.operationId, this.sourceEndpointInfo, this.targetEndpointInfo, azdata.TaskExecutionMode.execute, this.deploymentOptions); - if (!this.comparisonResult || !this.comparisonResult.success) { - TelemetryReporter.createErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaComparisonFailed', undefined, getTelemetryErrorType(this.comparisonResult?.errorMessage)) + 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) { + TelemetryReporter.createErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaComparisonFailed', undefined, getTelemetryErrorType(this.comparisonResult?.errorMessage)) + .withAdditionalProperties({ + operationId: this.comparisonResult.operationId + }).send(); + + vscode.window.showErrorMessage(loc.compareErrorMessage(this.comparisonResult?.errorMessage)); + + // reset state so a new comparison can be made + this.resetWindow(); + + return; + } + + TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaComparisonFinished') .withAdditionalProperties({ - operationId: this.comparisonResult.operationId + 'endTime': Date.now().toString(), + 'operationId': this.comparisonResult.operationId }).send(); - vscode.window.showErrorMessage(loc.compareErrorMessage(this.comparisonResult?.errorMessage)); - - // reset state so a new comparison can be made - this.resetWindow(); - - return; } - TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaComparisonFinished') - .withAdditionalProperties({ - 'endTime': Date.now().toString(), - 'operationId': this.comparisonResult.operationId - }).send(); let data = this.getAllDifferences(this.comparisonResult.differences); @@ -371,9 +419,15 @@ export class SchemaCompareMainWindow { // only enable generate script button if the target is a db if (this.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Database) { this.generateScriptButton.enabled = true; - this.applyButton.enabled = true; } else { this.generateScriptButton.title = loc.generateScriptDisabled; + } + + // only enable apply button if the target is a db or a project + if (this.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Database || + this.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Project) { + this.applyButton.enabled = true; + } else { this.applyButton.title = loc.applyDisabled; } } else { @@ -770,7 +824,6 @@ export class SchemaCompareMainWindow { } public async publishChanges(): Promise { - // need only yes button - since the modal dialog has a default cancel const yesString = loc.YesButtonText; await vscode.window.showWarningMessage(loc.applyConfirmation, { modal: true }, yesString).then(async (result) => { @@ -785,13 +838,25 @@ export class SchemaCompareMainWindow { this.setButtonsForRecompare(); const service: mssql.ISchemaCompareService = await this.getService(); - const result = await service.schemaComparePublishChanges(this.comparisonResult.operationId, this.targetEndpointInfo.serverName, this.targetEndpointInfo.databaseName, azdata.TaskExecutionMode.execute); + let result: azdata.ResultStatus | undefined = undefined; + + switch (this.targetEndpointInfo.endpointType) { + case mssql.SchemaCompareEndpointType.Database: + result = await service.schemaComparePublishDatabaseChanges(this.comparisonResult.operationId, this.targetEndpointInfo.serverName, this.targetEndpointInfo.databaseName, azdata.TaskExecutionMode.execute); + break; + case mssql.SchemaCompareEndpointType.Project: // Project apply needs sql-database-projects updates in (circular dependency; coming next) // TODO: re-add this and show project logic below + case mssql.SchemaCompareEndpointType.Dacpac: // Dacpac is an invalid publish target + default: + throw new Error(`Unsupported SchemaCompareEndpointType: ${getSchemaCompareEndpointString(this.targetEndpointInfo.endpointType)}`); + } + if (!result || !result.success) { - TelemetryReporter.createErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareApplyFailed', undefined, getTelemetryErrorType(result.errorMessage)) + + TelemetryReporter.createErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareApplyFailed', undefined, getTelemetryErrorType(result?.errorMessage)) .withAdditionalProperties({ 'operationId': this.comparisonResult.operationId }).send(); - vscode.window.showErrorMessage(loc.applyErrorMessage(result.errorMessage)); + vscode.window.showErrorMessage(loc.applyErrorMessage(result?.errorMessage)); // reenable generate script and apply buttons if apply failed this.generateScriptButton.enabled = true; @@ -799,6 +864,7 @@ export class SchemaCompareMainWindow { this.applyButton.enabled = true; this.applyButton.title = loc.applyEnabledMessage; } + TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareApplyEnded') .withAdditionalProperties({ 'endTime': Date.now().toString(), @@ -1090,11 +1156,15 @@ export class SchemaCompareMainWindow { } private setButtonStatesForNoChanges(enableButtons: boolean): void { - // generate script and apply can only be enabled if the target is a database - if (this.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Database) { + // generate script and apply can only be enabled if the target is a database or project + if (this.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Database || + this.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Project) { this.applyButton.enabled = enableButtons; - this.generateScriptButton.enabled = enableButtons; this.applyButton.title = enableButtons ? loc.applyEnabledMessage : loc.applyNoChangesMessage; + } + + if (this.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Database) { + this.generateScriptButton.enabled = enableButtons; this.generateScriptButton.title = enableButtons ? loc.generateScriptEnabledMessage : loc.generateScriptNoChangesMessage; } } diff --git a/extensions/schema-compare/src/test/schemaCompare.test.ts b/extensions/schema-compare/src/test/schemaCompare.test.ts index 4828d91bd1..1909d99d9a 100644 --- a/extensions/schema-compare/src/test/schemaCompare.test.ts +++ b/extensions/schema-compare/src/test/schemaCompare.test.ts @@ -110,7 +110,7 @@ describe('SchemaCompareMainWindow.results @DacFx@', function (): void { it('Should show error if publish changes fails', async function (): Promise { let service = createServiceMock(); - service.setup(x => x.schemaComparePublishChanges(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ + service.setup(x => x.schemaComparePublishDatabaseChanges(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ success: false, errorMessage: 'error1' })); @@ -121,7 +121,7 @@ describe('SchemaCompareMainWindow.results @DacFx@', function (): void { await schemaCompareResult.start(undefined); schemaCompareResult.sourceEndpointInfo = setDacpacEndpointInfo(mocksource); - schemaCompareResult.targetEndpointInfo = setDacpacEndpointInfo(mocktarget); + schemaCompareResult.targetEndpointInfo = setDatabaseEndpointInfo(); await schemaCompareResult.execute(); await schemaCompareResult.publishChanges(); @@ -131,7 +131,7 @@ describe('SchemaCompareMainWindow.results @DacFx@', function (): void { it('Should show not error if publish changes succeed', async function (): Promise { let service = createServiceMock(); - service.setup(x => x.schemaComparePublishChanges(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ + service.setup(x => x.schemaComparePublishDatabaseChanges(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ success: true, errorMessage: '' })); @@ -140,7 +140,7 @@ describe('SchemaCompareMainWindow.results @DacFx@', function (): void { await schemaCompareResult.start(undefined); schemaCompareResult.sourceEndpointInfo = setDacpacEndpointInfo(mocksource); - schemaCompareResult.targetEndpointInfo = setDacpacEndpointInfo(mocktarget); + schemaCompareResult.targetEndpointInfo = setDatabaseEndpointInfo(); await schemaCompareResult.execute(); await schemaCompareResult.publishChanges(); should(showErrorMessageSpy.notCalled).be.true(); @@ -343,7 +343,7 @@ describe('SchemaCompareMainWindow.results @DacFx@', function (): void { it('Should not show error if user does not want to publish', async function (): Promise { let service = createServiceMock(); - service.setup(x => x.schemaComparePublishChanges(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ + service.setup(x => x.schemaComparePublishDatabaseChanges(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ success: true, errorMessage: '' })); diff --git a/extensions/schema-compare/src/test/testUtils.ts b/extensions/schema-compare/src/test/testUtils.ts index cd61b340d5..26940ed4b1 100644 --- a/extensions/schema-compare/src/test/testUtils.ts +++ b/extensions/schema-compare/src/test/testUtils.ts @@ -98,7 +98,7 @@ export const mockDacpacEndpoint: mssql.SchemaCompareEndpointInfo = { projectFilePath: '', folderStructure: '', targetScripts: [], - dataSchemaProvider: '' + dataSchemaProvider: '', }; export const mockDatabaseEndpoint: mssql.SchemaCompareEndpointInfo = { @@ -112,7 +112,7 @@ export const mockDatabaseEndpoint: mssql.SchemaCompareEndpointInfo = { projectFilePath: '', folderStructure: '', targetScripts: [], - dataSchemaProvider: '' + dataSchemaProvider: '', }; export async function shouldThrowSpecificError(block: Function, expectedMessage: string, details?: string) { diff --git a/extensions/schema-compare/src/utils.ts b/extensions/schema-compare/src/utils.ts index f4f766d80c..d6be0a0e72 100644 --- a/extensions/schema-compare/src/utils.ts +++ b/extensions/schema-compare/src/utils.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; +import type * as azdataType from 'azdata'; // eslint-disable-line no-duplicate-imports import * as vscode from 'vscode'; import * as mssql from '../../mssql'; import * as os from 'os'; @@ -39,6 +40,19 @@ export function getTelemetryErrorType(msg: string): string { } } +export function getSchemaCompareEndpointString(endpointType: mssql.SchemaCompareEndpointType): string { + switch (endpointType) { + case mssql.SchemaCompareEndpointType.Database: + return 'Database'; + case mssql.SchemaCompareEndpointType.Dacpac: + return 'Dacpac'; + case mssql.SchemaCompareEndpointType.Project: + return 'Project'; + default: + return `Unknown: ${endpointType}`; + } +} + /** * Return the appropriate endpoint name depending on if the endpoint is a dacpac or a database * @param endpoint endpoint to get the name of @@ -64,8 +78,11 @@ export function getEndpointName(endpoint: mssql.SchemaCompareEndpointInfo): stri return ' '; } - } else { + } else if (endpoint.endpointType === mssql.SchemaCompareEndpointType.Dacpac) { return endpoint.packageFilePath; + + } else { + return endpoint.projectFilePath; } } @@ -144,3 +161,24 @@ export async function exists(path: string): Promise { return false; } } + +// Try to load the azdata API - but gracefully handle the failure in case we're running +// in a context where the API doesn't exist (such as VS Code) +let azdataApi: typeof azdataType | undefined = undefined; +try { + azdataApi = require('azdata'); + if (!azdataApi?.version) { + // webpacking makes the require return an empty object instead of throwing an error so make sure we clear the var + azdataApi = undefined; + } +} catch { + // no-op +} + +/** + * Gets the azdata API if it's available in the context this extension is running in. + * @returns The azdata API if it's available + */ +export function getAzdataApi(): typeof azdataType | undefined { + return azdataApi; +}