From b83279c24c6cdc8d3a4d422fb3c31fed9612bdc2 Mon Sep 17 00:00:00 2001 From: Kim Santiago <31145923+kisantia@users.noreply.github.com> Date: Fri, 15 May 2020 13:03:08 -0700 Subject: [PATCH] Add sql proj schema compare for dacpac (#10388) * add support for schema compare to specify source dacpac * add build and dacpac produced from build * check if dacpac exists * add tests * move exists check code to utils * fix test run failing --- .../src/controllers/mainController.ts | 2 +- .../src/schemaCompareMainWindow.ts | 15 +++++++ .../src/test/schemaCompare.test.ts | 42 +++++++++++++++++++ .../src/common/constants.ts | 3 ++ .../sql-database-projects/src/common/utils.ts | 14 +++++++ .../src/controllers/mainController.ts | 1 + .../src/controllers/projectController.ts | 21 ++++++++++ 7 files changed, 97 insertions(+), 1 deletion(-) diff --git a/extensions/schema-compare/src/controllers/mainController.ts b/extensions/schema-compare/src/controllers/mainController.ts index e26f67865c16..e84e47534272 100644 --- a/extensions/schema-compare/src/controllers/mainController.ts +++ b/extensions/schema-compare/src/controllers/mainController.ts @@ -30,7 +30,7 @@ export default class MainController implements vscode.Disposable { } private initializeSchemaCompareDialog(): void { - azdata.tasks.registerTask('schemaCompare.start', (profile: azdata.IConnectionProfile) => new SchemaCompareMainWindow(null, this.extensionContext).start(profile)); + vscode.commands.registerCommand('schemaCompare.start', (context: any) => new SchemaCompareMainWindow(null, this.extensionContext).start(context)); } public dispose(): void { diff --git a/extensions/schema-compare/src/schemaCompareMainWindow.ts b/extensions/schema-compare/src/schemaCompareMainWindow.ts index 2273450f75ea..21280b6bdc90 100644 --- a/extensions/schema-compare/src/schemaCompareMainWindow.ts +++ b/extensions/schema-compare/src/schemaCompareMainWindow.ts @@ -77,9 +77,14 @@ export class SchemaCompareMainWindow { this.editor = azdata.workspace.createModelViewEditor(loc.SchemaCompareLabel, { retainContextWhenHidden: true, supportsSave: true, resourceName: schemaCompareResourceName }); } + // schema compare can get started with three contexts for the source: + // 1. undefined + // 2. connection profile + // 3. dacpac public async start(context: any) { // if schema compare was launched from a db, set that as the source let profile = context ? context.connectionProfile : undefined; + let sourceDacpac = context as string; if (profile) { let ownerUri = await azdata.connection.getUriForConnection((profile.id)); this.sourceEndpointInfo = { @@ -91,6 +96,16 @@ export class SchemaCompareMainWindow { packageFilePath: '', connectionDetails: undefined }; + } else if (sourceDacpac) { + this.sourceEndpointInfo = { + endpointType: mssql.SchemaCompareEndpointType.Dacpac, + serverDisplayName: '', + serverName: '', + databaseName: '', + ownerUri: '', + packageFilePath: sourceDacpac, + connectionDetails: undefined + }; } this.editor.registerContent(async view => { diff --git a/extensions/schema-compare/src/test/schemaCompare.test.ts b/extensions/schema-compare/src/test/schemaCompare.test.ts index 7fff4ef10501..597e0d99691b 100644 --- a/extensions/schema-compare/src/test/schemaCompare.test.ts +++ b/extensions/schema-compare/src/test/schemaCompare.test.ts @@ -93,4 +93,46 @@ describe('SchemaCompareResult.start', function (): void { should(result.getComparisonResult() !== undefined); should(result.getComparisonResult().operationId === 'Test Operation Id'); }); + + it('Should start with the source as undefined', async function (): Promise { + let sc = new SchemaCompareTestService(); + + let result = new SchemaCompareMainWindow(sc, mockExtensionContext.object); + await result.start(undefined); + let promise = new Promise(resolve => setTimeout(resolve, 5000)); // to ensure comparison result view is initialized + await promise; + + should.equal(result.sourceEndpointInfo, undefined); + should.equal(result.targetEndpointInfo, undefined); + }); + + it('Should start with the source as database', async function (): Promise { + let sc = new SchemaCompareTestService(); + + let result = new SchemaCompareMainWindow(sc, mockExtensionContext.object); + await result.start({connectionProfile: mockConnectionProfile}); + let promise = new Promise(resolve => setTimeout(resolve, 5000)); // to ensure comparison result view is initialized + await promise; + + should.notEqual(result.sourceEndpointInfo, undefined); + should.equal(result.sourceEndpointInfo.endpointType, mssql.SchemaCompareEndpointType.Database); + should.equal(result.sourceEndpointInfo.serverName, mockConnectionProfile.serverName); + should.equal(result.sourceEndpointInfo.databaseName, mockConnectionProfile.databaseName); + should.equal(result.targetEndpointInfo, undefined); + }); + + it('Should start with the source as dacpac.', async function (): Promise { + let sc = new SchemaCompareTestService(); + + let result = new SchemaCompareMainWindow(sc, mockExtensionContext.object); + const dacpacPath = 'C:\\users\\test\\test.dacpac'; + await result.start(dacpacPath); + let promise = new Promise(resolve => setTimeout(resolve, 5000)); // to ensure comparison result view is initialized + await promise; + + should.notEqual(result.sourceEndpointInfo, undefined); + should.equal(result.sourceEndpointInfo.endpointType, mssql.SchemaCompareEndpointType.Dacpac); + should.equal(result.sourceEndpointInfo.packageFilePath, dacpacPath); + should.equal(result.targetEndpointInfo, undefined); + }); }); diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index b88b378dc855..721a7b71f11d 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -11,6 +11,7 @@ const localize = nls.loadMessageBundle(); export const dataSourcesFileName = 'datasources.json'; export const sqlprojExtension = '.sqlproj'; export const initialCatalogSetting = 'Initial Catalog'; +export const schemaCompareExtensionId = 'microsoft.schema-compare'; // UI Strings @@ -51,6 +52,8 @@ export const unknownDataSourceType = localize('unknownDataSourceType', "Unknown export const invalidSqlConnectionString = localize('invalidSqlConnectionString', "Invalid SQL connection string"); export const projectNameRequired = localize('projectNameRequired', "Name is required to create a new database project."); export const projectLocationRequired = localize('projectLocationRequired', "Location is required to create a new database project."); +export const schemaCompareNotInstalled = localize('schemaCompareNotInstalled', "Schema compare extension installation is required to run schema compare"); +export const buildDacpacNotFound = localize('buildDacpacNotFound', "Dacpac created from build not found"); export function projectAlreadyOpened(path: string) { return localize('projectAlreadyOpened', "Project '{0}' is already opened.", path); } export function projectAlreadyExists(name: string, path: string) { return localize('projectAlreadyExists', "A project named {0} already exists in {1}.", name, path); } export function mssqlNotFound(mssqlConfigDir: string) { return localize('mssqlNotFound', "Could not get mssql extension's install location at {0}", mssqlConfigDir); } diff --git a/extensions/sql-database-projects/src/common/utils.ts b/extensions/sql-database-projects/src/common/utils.ts index 78a3dac9f289..eb480195db29 100644 --- a/extensions/sql-database-projects/src/common/utils.ts +++ b/extensions/sql-database-projects/src/common/utils.ts @@ -5,6 +5,8 @@ import * as vscode from 'vscode'; import * as os from 'os'; +import { promises as fs } from 'fs'; + /** * Consolidates on the error message string */ @@ -74,3 +76,15 @@ export function getSafeNonWindowsPath(filePath: string): string { filePath = filePath.split('\\').join('/').split('"').join(''); return '"' + filePath + '"'; } + +/** + * Checks if the folder or file exists @param path path of the folder/file +*/ +export async function exists(path: string): Promise { + try { + await fs.access(path); + return true; + } catch (e) { + return false; + } +} diff --git a/extensions/sql-database-projects/src/controllers/mainController.ts b/extensions/sql-database-projects/src/controllers/mainController.ts index 4a566ab99665..b2961317b817 100644 --- a/extensions/sql-database-projects/src/controllers/mainController.ts +++ b/extensions/sql-database-projects/src/controllers/mainController.ts @@ -51,6 +51,7 @@ export default class MainController implements Disposable { this.apiWrapper.registerCommand('sqlDatabaseProjects.build', async (node: BaseProjectTreeItem) => { await this.projectsController.buildProject(node); }); this.apiWrapper.registerCommand('sqlDatabaseProjects.deploy', async (node: BaseProjectTreeItem) => { await this.projectsController.deploy(node); }); + this.apiWrapper.registerCommand('sqlDatabaseProjects.schemaCompare', async (node: BaseProjectTreeItem) => { await this.projectsController.schemaCompare(node); }); this.apiWrapper.registerCommand('sqlDatabaseProjects.import', async (node: BaseProjectTreeItem) => { await this.projectsController.import(node); }); this.apiWrapper.registerCommand('sqlDatabaseProjects.newScript', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.script); }); diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index 9377e1cea873..75d67995267d 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -138,6 +138,27 @@ export class ProjectsController { deployDatabaseDialog.openDialog(); } + public async schemaCompare(treeNode: BaseProjectTreeItem): Promise { + // check if schema compare extension is installed + if (this.apiWrapper.getExtension(constants.schemaCompareExtensionId)) { + // build project + await this.buildProject(treeNode); + + // start schema compare with the dacpac produced from build + const project = this.getProjectContextFromTreeNode(treeNode); + const dacpacPath = path.join(project.projectFolderPath, 'bin', 'Debug', `${project.projectFileName}.dacpac`); + + // check that dacpac exists + if (await utils.exists(dacpacPath)) { + this.apiWrapper.executeCommand('schemaCompare.start', dacpacPath); + } else { + this.apiWrapper.showErrorMessage(constants.buildDacpacNotFound); + } + } else { + this.apiWrapper.showErrorMessage(constants.schemaCompareNotInstalled); + } + } + public async import(treeNode: BaseProjectTreeItem) { const project = this.getProjectContextFromTreeNode(treeNode); await this.apiWrapper.showErrorMessage(`Import not yet implemented: ${project.projectFilePath}`); // TODO