diff --git a/extensions/sql-database-projects/package.json b/extensions/sql-database-projects/package.json index e38390426c..9052c29db3 100644 --- a/extensions/sql-database-projects/package.json +++ b/extensions/sql-database-projects/package.json @@ -160,6 +160,11 @@ "command": "sqlDatabaseProjects.editProjectFile", "title": "%sqlDatabaseProjects.editProjectFile%", "category": "%sqlDatabaseProjects.displayName%" + }, + { + "command": "sqlDatabaseProjects.changeTargetPlatform", + "title": "%sqlDatabaseProjects.changeTargetPlatform%", + "category": "%sqlDatabaseProjects.displayName%" } ], "menus": { @@ -244,6 +249,10 @@ { "command": "sqlDatabaseProjects.exclude", "when": "false" + }, + { + "command": "sqlDatabaseProjects.changeTargetPlatform", + "when": "false" } ], "view/title": [ @@ -329,6 +338,11 @@ "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.folder || viewItem == databaseProject.itemType.file || viewItem == databaseProject.itemType.reference", "group": "9_dbProjectsLast@2" }, + { + "command": "sqlDatabaseProjects.changeTargetPlatform", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project", + "group": "9_dbProjectsLast@6" + }, { "command": "sqlDatabaseProjects.editProjectFile", "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project", diff --git a/extensions/sql-database-projects/package.nls.json b/extensions/sql-database-projects/package.nls.json index 52a343d96f..230823fb51 100644 --- a/extensions/sql-database-projects/package.nls.json +++ b/extensions/sql-database-projects/package.nls.json @@ -27,6 +27,7 @@ "sqlDatabaseProjects.addDatabaseReference": "Add Database Reference", "sqlDatabaseProjects.openContainingFolder": "Open Containing Folder", "sqlDatabaseProjects.editProjectFile": "Edit .sqlproj File", + "sqlDatabaseProjects.changeTargetPlatform": "Change Target Platform", "sqlDatabaseProjects.Settings": "Database Projects", "sqlDatabaseProjects.netCoreInstallLocation": "Full path to .Net Core SDK on the machine.", diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 14a68cd380..38e6b7aaa2 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -69,6 +69,8 @@ export function newObjectNamePrompt(objectType: string) { return localize('newOb export function deleteConfirmation(toDelete: string) { return localize('deleteConfirmation', "Are you sure you want to delete {0}?", toDelete); } export function deleteConfirmationContents(toDelete: string) { return localize('deleteConfirmationContents', "Are you sure you want to delete {0} and all of its contents?", toDelete); } export function deleteReferenceConfirmation(toDelete: string) { return localize('deleteReferenceConfirmation', "Are you sure you want to delete the reference to {0}?", toDelete); } +export function selectTargetPlatform(currentTargetPlatform: string) { return localize('selectTargetPlatform', "Current target platform: {0}. Select new target platform", currentTargetPlatform); } +export function currentTargetPlatform(projectName: string, currentTargetPlatform: string) { return localize('currentTargetPlatform', "Target platform of the project {0} is now {1}", projectName, currentTargetPlatform); } // Publish dialog strings @@ -171,6 +173,7 @@ export function deployScriptExists(scriptType: string) { return localize('deploy export function notValidVariableName(name: string) { return localize('notValidVariableName', "The variable name '{0}' is not valid.", name); } export function cantAddCircularProjectReference(project: string) { return localize('cantAddCircularProjectReference', "A reference to project '{0} cannot be added. Adding this project as a reference would cause a circular dependency", project); } export function unableToFindSqlCmdVariable(variableName: string) { return localize('unableToFindSqlCmdVariable', "Unable to find SQLCMD variable '{0}'", variableName); } +export function unableToFindDatabaseReference(reference: string) { return localize('unableToFindReference', "Unable to find database reference {0}", reference); } // Action types export const deleteAction = localize('deleteAction', 'Delete'); @@ -286,3 +289,27 @@ export const systemDbs = ['master', 'msdb', 'tempdb', 'model']; export const sameDatabaseExampleUsage = 'SELECT * FROM [Schema1].[Table1]'; export function differentDbSameServerExampleUsage(db: string) { return `SELECT * FROM [${db}].[Schema1].[Table1]`; } export function differentDbDifferentServerExampleUsage(server: string, db: string) { return `SELECT * FROM [${server}].[${db}].[Schema1].[Table1]`; } + +export const sqlServer2005 = 'SQL Server 2005'; +export const sqlServer2008 = 'SQL Server 2008'; +export const sqlServer2012 = 'SQL Server 2012'; +export const sqlServer2014 = 'SQL Server 2014'; +export const sqlServer2016 = 'SQL Server 2016'; +export const sqlServer2017 = 'SQL Server 2017'; +export const sqlServer2019 = 'SQL Server 2019'; +export const sqlAzure = 'Microsoft Azure SQL Database'; + +export const targetPlatformToVersion: Map = new Map([ + [sqlServer2005, '90'], + [sqlServer2008, '100'], + [sqlServer2012, '110'], + [sqlServer2014, '120'], + [sqlServer2016, '130'], + [sqlServer2017, '140'], + [sqlServer2019, '150'], + [sqlAzure, 'AzureV12'] +]); + +export function getTargetPlatformFromVersion(version: string): string { + return Array.from(targetPlatformToVersion.keys()).filter(k => targetPlatformToVersion.get(k) === version)[0]; +} diff --git a/extensions/sql-database-projects/src/controllers/mainController.ts b/extensions/sql-database-projects/src/controllers/mainController.ts index a1fe163283..681f967812 100644 --- a/extensions/sql-database-projects/src/controllers/mainController.ts +++ b/extensions/sql-database-projects/src/controllers/mainController.ts @@ -78,6 +78,7 @@ export default class MainController implements vscode.Disposable { vscode.commands.registerCommand('sqlDatabaseProjects.editProjectFile', async (node: BaseProjectTreeItem) => { await this.projectsController.editProjectFile(node); }); vscode.commands.registerCommand('sqlDatabaseProjects.delete', async (node: BaseProjectTreeItem) => { await this.projectsController.delete(node); }); vscode.commands.registerCommand('sqlDatabaseProjects.exclude', async (node: FileNode | FolderNode) => { await this.projectsController.exclude(node); }); + vscode.commands.registerCommand('sqlDatabaseProjects.changeTargetPlatform', async (node: BaseProjectTreeItem) => { await this.projectsController.changeTargetPlatform(node); }); IconPathHelper.setExtensionContext(this.extensionContext); diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index f587c2d489..bcd3cf8e0f 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -515,6 +515,24 @@ export class ProjectsController { } } + /** + * Changes the project's DSP to the selected target platform + * @param context a treeItem in a project's hierarchy, to be used to obtain a Project + */ + public async changeTargetPlatform(context: Project | BaseProjectTreeItem): Promise { + const project = this.getProjectFromContext(context); + const selectedTargetPlatform = (await vscode.window.showQuickPick((Array.from(constants.targetPlatformToVersion.keys())).map(version => { return { label: version }; }), + { + canPickMany: false, + placeHolder: constants.selectTargetPlatform(constants.getTargetPlatformFromVersion(project.getProjectTargetVersion())) + }))?.label; + + if (selectedTargetPlatform) { + await project.changeTargetPlatform(constants.targetPlatformToVersion.get(selectedTargetPlatform)!); + vscode.window.showInformationMessage(constants.currentTargetPlatform(project.projectFileName, constants.getTargetPlatformFromVersion(project.getProjectTargetVersion()))); + } + } + /** * Adds a database reference to the project * @param context a treeItem in a project's hierarchy, to be used to obtain a Project @@ -592,7 +610,7 @@ export class ProjectsController { } } - private getProjectFromContext(context: Project | BaseProjectTreeItem | WorkspaceTreeItem) { + private getProjectFromContext(context: Project | BaseProjectTreeItem | WorkspaceTreeItem): Project { if ('element' in context) { return context.element.project; } diff --git a/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts b/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts index 9776b24519..ebe3730044 100644 --- a/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts @@ -300,7 +300,7 @@ export class AddDatabaseReferenceDialog { }); // only master is a valid system db reference for projects targetting Azure - if (this.project.getProjectTargetPlatform().toLowerCase().includes('azure')) { + if (this.project.getProjectTargetVersion().toLowerCase().includes('azure')) { this.systemDatabaseDropdown.values?.splice(1); } diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts index 7bbb7c650e..88b0e6907a 100644 --- a/extensions/sql-database-projects/src/models/project.ts +++ b/extensions/sql-database-projects/src/models/project.ts @@ -326,13 +326,33 @@ export class Project { } /** - * Set the compat level of the project - * Just used in tests right now, but can be used later if this functionality is added to the UI - * @param compatLevel compat level of project + * Set the target platform of the project + * @param newTargetPlatform compat level of project */ - public changeDSP(compatLevel: string): void { - const newDSP = `${constants.MicrosoftDatatoolsSchemaSqlSql}${compatLevel}${constants.databaseSchemaProvider}`; - this.projFileXmlDoc.getElementsByTagName(constants.DSP)[0].childNodes[0].nodeValue = newDSP; + public async changeTargetPlatform(compatLevel: string): Promise { + if (this.getProjectTargetVersion() !== compatLevel) { + const newDSP = `${constants.MicrosoftDatatoolsSchemaSqlSql}${compatLevel}${constants.databaseSchemaProvider}`; + this.projFileXmlDoc.getElementsByTagName(constants.DSP)[0].childNodes[0].data = newDSP; + this.projFileXmlDoc.getElementsByTagName(constants.DSP)[0].childNodes[0].nodeValue = newDSP; + + // update any system db references + const systemDbReferences = this.databaseReferences.filter(r => r instanceof SystemDatabaseReferenceProjectEntry) as SystemDatabaseReferenceProjectEntry[]; + if (systemDbReferences.length > 0) { + systemDbReferences.forEach((r) => { + // remove old entry in sqlproj + this.removeDatabaseReferenceFromProjFile(r); + + // update uris to point to the correct dacpacs for the target platform + r.fsUri = this.getSystemDacpacUri(`${r.databaseName}.dacpac`); + r.ssdtUri = this.getSystemDacpacSsdtUri(`${r.databaseName}.dacpac`); + + // add updated system db reference to sqlproj + this.addDatabaseReferenceToProjFile(r); + }); + } + + await this.serializeToProjFile(this.projFileXmlDoc); + } } /** @@ -351,26 +371,32 @@ export class Project { } const systemDatabaseReferenceProjectEntry = new SystemDatabaseReferenceProjectEntry(uri, ssdtUri, settings.databaseName, settings.suppressMissingDependenciesErrors); + + // check if reference to this database already exists + if (this.databaseReferenceExists(systemDatabaseReferenceProjectEntry)) { + throw new Error(constants.databaseReferenceAlreadyExists); + } + await this.addToProjFile(systemDatabaseReferenceProjectEntry); } public getSystemDacpacUri(dacpac: string): Uri { - let version = this.getProjectTargetPlatform(); + let version = this.getProjectTargetVersion(); return Uri.parse(path.join('$(NETCoreTargetsPath)', 'SystemDacpacs', version, dacpac)); } public getSystemDacpacSsdtUri(dacpac: string): Uri { - let version = this.getProjectTargetPlatform(); + let version = this.getProjectTargetVersion(); return Uri.parse(path.join('$(DacPacRootPath)', 'Extensions', 'Microsoft', 'SQLDB', 'Extensions', 'SqlServer', version, 'SqlSchemas', dacpac)); } - public getProjectTargetPlatform(): string { + public getProjectTargetVersion(): string { // check for invalid DSP if (this.projFileXmlDoc.getElementsByTagName(constants.DSP).length !== 1 || this.projFileXmlDoc.getElementsByTagName(constants.DSP)[0].childNodes.length !== 1) { throw new Error(constants.invalidDataSchemaProvider); } - let dsp: string = this.projFileXmlDoc.getElementsByTagName(constants.DSP)[0].childNodes[0].nodeValue; + let dsp: string = this.projFileXmlDoc.getElementsByTagName(constants.DSP)[0].childNodes[0].data; // get version from dsp, which is a string like Microsoft.Data.Tools.Schema.Sql.Sql130DatabaseSchemaProvider // remove part before the number @@ -379,7 +405,7 @@ export class Project { version = version.substring(0, version.length - constants.databaseSchemaProvider.length); // make sure version is valid - if (!Object.values(TargetPlatform).includes(version)) { + if (!Array.from(constants.targetPlatformToVersion.values()).includes(version)) { throw new Error(constants.invalidDataSchemaProvider); } @@ -393,6 +419,12 @@ export class Project { */ public async addDatabaseReference(settings: IDacpacReferenceSettings): Promise { const databaseReferenceEntry = new DacpacReferenceProjectEntry(settings); + + // check if reference to this database already exists + if (this.databaseReferenceExists(databaseReferenceEntry)) { + throw new Error(constants.databaseReferenceAlreadyExists); + } + await this.addToProjFile(databaseReferenceEntry); } @@ -546,7 +578,7 @@ export class Project { } if (!deleted) { - throw new Error(constants.unableToFindSqlCmdVariable(databaseReferenceEntry.databaseName)); + throw new Error(constants.unableToFindDatabaseReference(databaseReferenceEntry.databaseName)); } } @@ -558,7 +590,6 @@ export class Project { systemDbReferenceNode.setAttribute(constants.Include, entry.pathForSqlProj()); this.addDatabaseReferenceChildren(systemDbReferenceNode, entry); this.findOrCreateItemGroup(constants.ArtifactReference).appendChild(systemDbReferenceNode); - this.databaseReferences.push(entry); // add a reference to the system dacpac in SSDT if it's a system db const ssdtReferenceNode = this.projFileXmlDoc.createElement(constants.ArtifactReference); @@ -569,11 +600,6 @@ export class Project { } private addDatabaseReferenceToProjFile(entry: IDatabaseReferenceProjectEntry): void { - // check if reference to this database already exists - if (this.databaseReferenceExists(entry)) { - throw new Error(constants.databaseReferenceAlreadyExists); - } - if (entry instanceof SystemDatabaseReferenceProjectEntry) { this.addSystemDatabaseReferenceToProjFile(entry); } else if (entry instanceof SqlProjectReferenceProjectEntry) { @@ -582,12 +608,14 @@ export class Project { this.addProjectReferenceChildren(referenceNode, entry); this.addDatabaseReferenceChildren(referenceNode, entry); this.findOrCreateItemGroup(constants.ProjectReference).appendChild(referenceNode); - this.databaseReferences.push(entry); } else { const referenceNode = this.projFileXmlDoc.createElement(constants.ArtifactReference); referenceNode.setAttribute(constants.Include, entry.pathForSqlProj()); this.addDatabaseReferenceChildren(referenceNode, entry); this.findOrCreateItemGroup(constants.ArtifactReference).appendChild(referenceNode); + } + + if (!this.databaseReferenceExists(entry)) { this.databaseReferences.push(entry); } } @@ -987,17 +1015,6 @@ export enum DatabaseReferenceLocation { differentDatabaseDifferentServer } -export enum TargetPlatform { - Sql90 = '90', - Sql100 = '100', - Sql110 = '110', - Sql120 = '120', - Sql130 = '130', - Sql140 = '140', - Sql150 = '150', - SqlAzureV12 = 'AzureV12' -} - export enum SystemDatabase { master, msdb diff --git a/extensions/sql-database-projects/src/test/project.test.ts b/extensions/sql-database-projects/src/test/project.test.ts index c337d81e33..adbd9e5745 100644 --- a/extensions/sql-database-projects/src/test/project.test.ts +++ b/extensions/sql-database-projects/src/test/project.test.ts @@ -12,7 +12,7 @@ import * as testUtils from './testUtils'; import * as constants from '../common/constants'; import { promises as fs } from 'fs'; -import { Project, EntryType, TargetPlatform, SystemDatabase, SystemDatabaseReferenceProjectEntry, SqlProjectReferenceProjectEntry } from '../models/project'; +import { Project, EntryType, SystemDatabase, SystemDatabaseReferenceProjectEntry, SqlProjectReferenceProjectEntry } from '../models/project'; import { exists, convertSlashesForSqlProj } from '../common/utils'; import { Uri, window } from 'vscode'; @@ -144,13 +144,13 @@ describe('Project: sqlproj content operations', function (): void { should.equal(uri.fsPath, Uri.parse(path.join('$(NETCoreTargetsPath)', 'SystemDacpacs', '150', constants.masterDacpac)).fsPath); should.equal(ssdtUri.fsPath, Uri.parse(path.join('$(DacPacRootPath)', 'Extensions', 'Microsoft', 'SQLDB', 'Extensions', 'SqlServer', '150', 'SqlSchemas', constants.masterDacpac)).fsPath); - project.changeDSP(TargetPlatform.Sql130.toString()); + project.changeTargetPlatform(constants.targetPlatformToVersion.get(constants.sqlServer2016)!); uri = project.getSystemDacpacUri(constants.masterDacpac); ssdtUri = project.getSystemDacpacSsdtUri(constants.masterDacpac); should.equal(uri.fsPath, Uri.parse(path.join('$(NETCoreTargetsPath)', 'SystemDacpacs', '130', constants.masterDacpac)).fsPath); should.equal(ssdtUri.fsPath, Uri.parse(path.join('$(DacPacRootPath)', 'Extensions', 'Microsoft', 'SQLDB', 'Extensions', 'SqlServer', '130', 'SqlSchemas', constants.masterDacpac)).fsPath); - project.changeDSP(TargetPlatform.SqlAzureV12.toString()); + project.changeTargetPlatform(constants.targetPlatformToVersion.get(constants.sqlAzure)!); uri = project.getSystemDacpacUri(constants.masterDacpac); ssdtUri = project.getSystemDacpacSsdtUri(constants.masterDacpac); should.equal(uri.fsPath, Uri.parse(path.join('$(NETCoreTargetsPath)', 'SystemDacpacs', 'AzureV12', constants.masterDacpac)).fsPath); @@ -166,13 +166,13 @@ describe('Project: sqlproj content operations', function (): void { should.equal(uri.fsPath, Uri.parse(path.join('$(NETCoreTargetsPath)', 'SystemDacpacs', '150', constants.msdbDacpac)).fsPath); should.equal(ssdtUri.fsPath, Uri.parse(path.join('$(DacPacRootPath)', 'Extensions', 'Microsoft', 'SQLDB', 'Extensions', 'SqlServer', '150', 'SqlSchemas', constants.msdbDacpac)).fsPath); - project.changeDSP(TargetPlatform.Sql130.toString()); + project.changeTargetPlatform(constants.targetPlatformToVersion.get(constants.sqlServer2016)!); uri = project.getSystemDacpacUri(constants.msdbDacpac); ssdtUri = project.getSystemDacpacSsdtUri(constants.msdbDacpac); should.equal(uri.fsPath, Uri.parse(path.join('$(NETCoreTargetsPath)', 'SystemDacpacs', '130', constants.msdbDacpac)).fsPath); should.equal(ssdtUri.fsPath, Uri.parse(path.join('$(DacPacRootPath)', 'Extensions', 'Microsoft', 'SQLDB', 'Extensions', 'SqlServer', '130', 'SqlSchemas', constants.msdbDacpac)).fsPath); - project.changeDSP(TargetPlatform.SqlAzureV12.toString()); + project.changeTargetPlatform(constants.targetPlatformToVersion.get(constants.sqlAzure)!); uri = project.getSystemDacpacUri(constants.msdbDacpac); ssdtUri = project.getSystemDacpacSsdtUri(constants.msdbDacpac); should.equal(uri.fsPath, Uri.parse(path.join('$(NETCoreTargetsPath)', 'SystemDacpacs', 'AzureV12', constants.msdbDacpac)).fsPath); @@ -183,7 +183,7 @@ describe('Project: sqlproj content operations', function (): void { projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); const project = await Project.openProject(projFilePath); - project.changeDSP('invalidPlatform'); + project.changeTargetPlatform('invalidPlatform'); await testUtils.shouldThrowSpecificError(async () => await project.getSystemDacpacUri(constants.masterDacpac), constants.invalidDataSchemaProvider); }); diff --git a/extensions/sql-database-projects/src/test/projectController.test.ts b/extensions/sql-database-projects/src/test/projectController.test.ts index f721480d40..abd47a297c 100644 --- a/extensions/sql-database-projects/src/test/projectController.test.ts +++ b/extensions/sql-database-projects/src/test/projectController.test.ts @@ -19,7 +19,7 @@ import { SqlDatabaseProjectTreeViewProvider } from '../controllers/databaseProje import { ProjectsController } from '../controllers/projectController'; import { promises as fs } from 'fs'; import { createContext, TestContext, mockDacFxResult } from './testContext'; -import { Project, reservedProjectFolders, SystemDatabase, FileProjectEntry } from '../models/project'; +import { Project, reservedProjectFolders, SystemDatabase, FileProjectEntry, SystemDatabaseReferenceProjectEntry } from '../models/project'; import { PublishDatabaseDialog } from '../dialogs/publishDatabaseDialog'; import { IPublishSettings, IGenerateScriptSettings } from '../models/IPublishSettings'; import { exists } from '../common/utils'; @@ -253,7 +253,7 @@ describe('ProjectsController', function (): void { proj.addProjectReference({ projectName: 'project1', projectGuid: '', - projectRelativePath: vscode.Uri.file(path.join('..','project1', 'project1.sqlproj')), + projectRelativePath: vscode.Uri.file(path.join('..', 'project1', 'project1.sqlproj')), suppressMissingDependenciesErrors: false }); @@ -335,6 +335,24 @@ describe('ProjectsController', function (): void { await projController.addItemPrompt(project, '', templates.postDeployScript); should(project.postDeployScripts.length).equal(1, 'Post deploy script should be successfully added'); }); + + it('Should change target platform', async function (): Promise { + sinon.stub(vscode.window, 'showQuickPick').resolves({ label: constants.sqlAzure }); + + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline); + const project = await projController.openProject(vscode.Uri.file(sqlProjPath)); + should(project.getProjectTargetVersion()).equal(constants.targetPlatformToVersion.get(constants.sqlServer2019)); + should(project.databaseReferences.length).equal(1, 'Project should have one database reference to master'); + should(project.databaseReferences[0].fsUri.fsPath).containEql(constants.targetPlatformToVersion.get(constants.sqlServer2019)); + should((project.databaseReferences[0]).ssdtUri.fsPath).containEql(constants.targetPlatformToVersion.get(constants.sqlServer2019)); + + await projController.changeTargetPlatform(project); + should(project.getProjectTargetVersion()).equal(constants.targetPlatformToVersion.get(constants.sqlAzure)); + // verify system db reference got updated too + should(project.databaseReferences[0].fsUri.fsPath).containEql(constants.targetPlatformToVersion.get(constants.sqlAzure)); + should((project.databaseReferences[0]).ssdtUri.fsPath).containEql(constants.targetPlatformToVersion.get(constants.sqlAzure)); + }); }); describe('Publishing and script generation', function (): void {