diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts
index 85400a64cd..9b4c3dd184 100644
--- a/extensions/sql-database-projects/src/common/constants.ts
+++ b/extensions/sql-database-projects/src/common/constants.ts
@@ -14,6 +14,7 @@ const localize = nls.loadMessageBundle();
export const dataSourcesFileName = 'datasources.json';
export const sqlprojExtension = '.sqlproj';
export const sqlFileExtension = '.sql';
+export const publishProfileExtension = '.publish.xml';
export const openApiSpecFileExtensions = ['yaml', 'yml', 'json'];
export const schemaCompareExtensionId = 'microsoft.schema-compare';
export const master = 'master';
@@ -485,6 +486,7 @@ export const ImportElements = localize('importElements', "Import Elements");
export const ProjectReferenceNameElement = localize('projectReferenceNameElement', "Project reference name element");
export const ProjectReferenceElement = localize('projectReferenceElement', "Project reference");
export const DacpacReferenceElement = localize('dacpacReferenceElement', "Dacpac reference");
+export const PublishProfileElements = localize('publishProfileElements', "Publish profile elements");
/** Name of the property item in the project file that defines default database collation. */
export const DefaultCollationProperty = 'DefaultCollation';
diff --git a/extensions/sql-database-projects/src/common/utils.ts b/extensions/sql-database-projects/src/common/utils.ts
index dc9341d863..e6839b03bb 100644
--- a/extensions/sql-database-projects/src/common/utils.ts
+++ b/extensions/sql-database-projects/src/common/utils.ts
@@ -771,3 +771,13 @@ export function isValidBasename(name?: string): boolean {
export function isValidBasenameErrorMessage(name?: string): string {
return getDataWorkspaceExtensionApi().isValidBasenameErrorMessage(name);
}
+
+/**
+ * Checks if the provided file is a publish profile
+ * @param fileName filename to check
+ * @returns True if it is a publish profile, otherwise false
+ */
+export function isPublishProfile(fileName: string): boolean {
+ const hasPublishExtension = fileName.trim().toLowerCase().endsWith(constants.publishProfileExtension);
+ return hasPublishExtension;
+}
diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts
index 18361ed0d3..163c69b95a 100644
--- a/extensions/sql-database-projects/src/controllers/projectController.ts
+++ b/extensions/sql-database-projects/src/controllers/projectController.ts
@@ -1444,7 +1444,7 @@ export class ProjectsController {
if (fileOrFolder) {
// use relative path and not tree paths for files and folder
- const allFileEntries = project.files.concat(project.preDeployScripts).concat(project.postDeployScripts).concat(project.noneDeployScripts);
+ const allFileEntries = project.files.concat(project.preDeployScripts).concat(project.postDeployScripts).concat(project.noneDeployScripts).concat(project.publishProfiles);
// trim trailing slash since folders with and without a trailing slash are allowed in a sqlproj
const trimmedUri = utils.trimChars(utils.getPlatformSafeFileEntryPath(utils.trimUri(fileOrFolder.projectFileUri, fileOrFolder.fileSystemUri)), '/');
diff --git a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts
index 18dbf3f561..92a36d12ef 100644
--- a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts
+++ b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts
@@ -947,6 +947,9 @@ export class PublishDatabaseDialog {
this.profileUsed = true;
this.publishProfileUri = filePath;
+
+ await this.project.addPublishProfileToProjFile(filePath.fsPath);
+ void vscode.commands.executeCommand(constants.refreshDataWorkspaceCommand); //refresh data workspace to load the newly added profile to the tree
});
return saveProfileAsButton;
diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts
index 762e798904..26cc229e35 100644
--- a/extensions/sql-database-projects/src/models/project.ts
+++ b/extensions/sql-database-projects/src/models/project.ts
@@ -46,6 +46,7 @@ export class Project implements ISqlProject {
private _isSdkStyleProject: boolean = false; // https://docs.microsoft.com/en-us/dotnet/core/project-sdk/overview
private _outputPath: string = '';
private _configuration: Configuration = Configuration.Debug;
+ private _publishProfiles: FileProjectEntry[] = [];
public get dacpacOutputPath(): string {
return path.join(this.outputPath, `${this._projectFileName}.dacpac`);
@@ -111,6 +112,10 @@ export class Project implements ISqlProject {
return this._configuration;
}
+ public get publishProfiles(): FileProjectEntry[] {
+ return this._publishProfiles;
+ }
+
private projFileXmlDoc: Document | undefined = undefined;
constructor(projectFilePath: string) {
@@ -153,6 +158,9 @@ export class Project implements ISqlProject {
this._databaseReferences = this.readDatabaseReferences();
this._importedTargets = this.readImportedTargets();
+ // get publish profiles specified in the sqlproj
+ this._publishProfiles = this.readPublishProfiles();
+
// find all SQLCMD variables to include
try {
this._sqlCmdVariables = utils.readSqlCmdVariables(this.projFileXmlDoc, false);
@@ -468,7 +476,7 @@ export class Project implements ISqlProject {
const noneItems = itemGroup.getElementsByTagName(constants.None);
for (let n = 0; n < noneItems.length; n++) {
const includeAttribute = noneItems[n].getAttribute(constants.Include);
- if (includeAttribute) {
+ if (includeAttribute && !utils.isPublishProfile(includeAttribute)) {
noneDeployScripts.push(this.createFileProjectEntry(includeAttribute, EntryType.File));
}
}
@@ -505,6 +513,34 @@ export class Project implements ISqlProject {
return noneRemoveScripts;
}
+ /**
+ *
+ * @returns all the publish profiles (ending with *.publish.xml) specified as in the sqlproj
+ */
+ private readPublishProfiles(): FileProjectEntry[] {
+ const publishProfiles: FileProjectEntry[] = [];
+
+ for (let ig = 0; ig < this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) {
+ const itemGroup = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup)[ig];
+
+ // find all publish profile scripts to include
+ try {
+ const noneItems = itemGroup.getElementsByTagName(constants.None);
+ for (let n = 0; n < noneItems.length; n++) {
+ const includeAttribute = noneItems[n].getAttribute(constants.Include);
+ if (includeAttribute && utils.isPublishProfile(includeAttribute)) {
+ publishProfiles.push(this.createFileProjectEntry(includeAttribute, EntryType.File));
+ }
+ }
+ } catch (e) {
+ void window.showErrorMessage(constants.errorReadingProject(constants.PublishProfileElements, this.projectFilePath));
+ console.error(utils.getErrorMessage(e));
+ }
+ }
+
+ return publishProfiles;
+ }
+
private readDatabaseReferences(): IDatabaseReferenceProjectEntry[] {
const databaseReferenceEntries: IDatabaseReferenceProjectEntry[] = [];
@@ -949,18 +985,19 @@ export class Project implements ISqlProject {
}
public async exclude(entry: FileProjectEntry): Promise {
- const toExclude: FileProjectEntry[] = this._files.concat(this._preDeployScripts).concat(this._postDeployScripts).concat(this._noneDeployScripts).filter(x => x.fsUri.fsPath.startsWith(entry.fsUri.fsPath));
+ const toExclude: FileProjectEntry[] = this._files.concat(this._preDeployScripts).concat(this._postDeployScripts).concat(this._noneDeployScripts).concat(this._publishProfiles).filter(x => x.fsUri.fsPath.startsWith(entry.fsUri.fsPath));
await this.removeFromProjFile(toExclude);
this._files = this._files.filter(x => !x.fsUri.fsPath.startsWith(entry.fsUri.fsPath));
this._preDeployScripts = this._preDeployScripts.filter(x => !x.fsUri.fsPath.startsWith(entry.fsUri.fsPath));
this._postDeployScripts = this._postDeployScripts.filter(x => !x.fsUri.fsPath.startsWith(entry.fsUri.fsPath));
this._noneDeployScripts = this._noneDeployScripts.filter(x => !x.fsUri.fsPath.startsWith(entry.fsUri.fsPath));
+ this._publishProfiles = this._publishProfiles.filter(x => !x.fsUri.fsPath.startsWith(entry.fsUri.fsPath));
}
public async deleteFileFolder(entry: FileProjectEntry): Promise {
// compile a list of folder contents to delete; if entry is a file, contents will contain only itself
- const toDeleteFiles: FileProjectEntry[] = this._files.concat(this._preDeployScripts).concat(this._postDeployScripts).concat(this._noneDeployScripts).filter(x => x.fsUri.fsPath.startsWith(entry.fsUri.fsPath) && x.type === EntryType.File);
+ const toDeleteFiles: FileProjectEntry[] = this._files.concat(this._preDeployScripts).concat(this._postDeployScripts).concat(this._noneDeployScripts).concat(this._publishProfiles).filter(x => x.fsUri.fsPath.startsWith(entry.fsUri.fsPath) && x.type === EntryType.File);
const toDeleteFolders: FileProjectEntry[] = this._files.filter(x => x.fsUri.fsPath.startsWith(entry.fsUri.fsPath) && x.type === EntryType.Folder);
await Promise.all(toDeleteFiles.map(x => fs.unlink(x.fsUri.fsPath)));
@@ -1165,6 +1202,24 @@ export class Project implements ISqlProject {
return this.getCollectionProjectPropertyValue(constants.DatabaseSource);
}
+ /**
+ * Adds publish profile to the project
+ *
+ * @param relativeFilePath Relative path of the file
+ */
+ public async addPublishProfileToProjFile(absolutePublishProfilePath: string): Promise {
+ const relativePublishProfilePath = (utils.trimUri(Uri.file(this.projectFilePath), Uri.file(absolutePublishProfilePath)));
+
+ // Update sqlproj XML
+
+ const fileEntry = this.createFileProjectEntry(relativePublishProfilePath, EntryType.File);
+ this._publishProfiles.push(fileEntry);
+
+ await this.addToProjFile(fileEntry, constants.None);
+
+ return fileEntry;
+ }
+
public createFileProjectEntry(relativePath: string, entryType: EntryType, sqlObjectType?: string, containsCreateTableStatement?: boolean): FileProjectEntry {
let platformSafeRelativePath = utils.getPlatformSafeFileEntryPath(relativePath);
return new FileProjectEntry(
@@ -1176,7 +1231,8 @@ export class Project implements ISqlProject {
}
private findOrCreateItemGroup(containedTag?: string, prePostScriptExist?: { scriptExist: boolean; }): Element {
- let outputItemGroup = undefined;
+ let outputItemGroup: Element[] = []; // "None" can have more than one ItemGroup, for "None Include" (for pre/post deploy scripts and publish profiles), "None Remove"
+ let returnItemGroup;
// search for a particular item goup if a child type is provided
if (containedTag) {
@@ -1184,25 +1240,45 @@ export class Project implements ISqlProject {
for (let ig = 0; ig < this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) {
const currentItemGroup = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup)[ig];
- // if we find the tag, use the ItemGroup
if (currentItemGroup.getElementsByTagName(containedTag).length > 0) {
- outputItemGroup = currentItemGroup;
- break;
+ outputItemGroup.push(currentItemGroup);
}
}
}
// if none already exist, make a new ItemGroup for it
- if (!outputItemGroup) {
- outputItemGroup = this.projFileXmlDoc!.createElement(constants.ItemGroup);
- this.projFileXmlDoc!.documentElement.appendChild(outputItemGroup);
+ if (outputItemGroup.length === 0) {
+ returnItemGroup = this.projFileXmlDoc!.createElement(constants.ItemGroup);
+ this.projFileXmlDoc!.documentElement.appendChild(returnItemGroup);
if (prePostScriptExist) {
prePostScriptExist.scriptExist = false;
}
+ } else { // if item group exists and containedTag = None, read the content to find None Include with publish profile
+ if (containedTag === constants.None) {
+ for (let ig = 0; ig < outputItemGroup.length; ig++) {
+ const itemGroup = outputItemGroup[ig];
+
+ // find all none include scripts specified in the sqlproj
+ const noneItems = itemGroup.getElementsByTagName(constants.None);
+ for (let n = 0; n < noneItems.length; n++) {
+ let noneIncludeItem = noneItems[n].getAttribute(constants.Include);
+ if (noneIncludeItem && utils.isPublishProfile(noneIncludeItem)) {
+ returnItemGroup = itemGroup;
+ break;
+ }
+ }
+ }
+ if (!returnItemGroup) {
+ returnItemGroup = this.projFileXmlDoc!.createElement(constants.ItemGroup);
+ this.projFileXmlDoc!.documentElement.appendChild(returnItemGroup);
+ }
+ } else {
+ returnItemGroup = outputItemGroup[0]; // Return the first item group that was found, to match prior implementation
+ }
}
- return outputItemGroup;
+ return returnItemGroup;
}
private async addFileToProjFile(filePath: string, xmlTag: string, attributes?: Map): Promise {
@@ -1220,6 +1296,8 @@ export class Project implements ISqlProject {
void window.showInformationMessage(constants.deployScriptExists(xmlTag));
xmlTag = constants.None; // Add only one pre-deploy and post-deploy script. All additional ones get added in the same item group with None tag
}
+ } else if (xmlTag === constants.None) { // Add publish profiles with None tag
+ itemGroup = this.findOrCreateItemGroup(xmlTag);
}
else {
if (this.isSdkStyleProject) {
@@ -1260,7 +1338,7 @@ export class Project implements ISqlProject {
itemGroup.appendChild(newFileNode);
}
- private async removeFileFromProjFile(path: string): Promise {
+ private async removeFileFromProjFile(path: string): Promise {//TODO: publish profile
const fileNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Build);
const preDeployNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.PreDeploy);
const postDeployNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.PostDeploy);
diff --git a/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts b/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts
index d80d3cf77d..3111aa511f 100644
--- a/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts
+++ b/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts
@@ -65,7 +65,8 @@ export class ProjectRootTreeItem extends BaseProjectTreeItem {
let treeItemList = this.project.files
.concat(this.project.preDeployScripts)
.concat(this.project.postDeployScripts)
- .concat(this.project.noneDeployScripts);
+ .concat(this.project.noneDeployScripts)
+ .concat(this.project.publishProfiles);
for (const entry of treeItemList) {
if (entry.type !== EntryType.File && entry.relativePath.startsWith(RelativeOuterPath)) {
diff --git a/extensions/sql-database-projects/src/test/baselines/baselines.ts b/extensions/sql-database-projects/src/test/baselines/baselines.ts
index b0a1da1d8c..d97089338d 100644
--- a/extensions/sql-database-projects/src/test/baselines/baselines.ts
+++ b/extensions/sql-database-projects/src/test/baselines/baselines.ts
@@ -40,6 +40,7 @@ export let openSdkStyleSqlProjectWithFilesSpecifiedBaseline: string;
export let openSdkStyleSqlProjectWithGlobsSpecifiedBaseline: string;
export let openSdkStyleSqlProjectWithBuildRemoveBaseline: string;
export let openSdkStyleSqlProjectNoProjectGuidBaseline: string;
+export let openSqlProjectWithAdditionalPublishProfileBaseline: string;
const baselineFolderPath = __dirname;
@@ -77,6 +78,7 @@ export async function loadBaselines() {
openSdkStyleSqlProjectWithGlobsSpecifiedBaseline = await loadBaseline(baselineFolderPath, 'openSdkStyleSqlProjectWithGlobsSpecifiedBaseline.xml');
openSdkStyleSqlProjectWithBuildRemoveBaseline = await loadBaseline(baselineFolderPath, 'openSdkStyleSqlProjectWithBuildRemoveBaseline.xml');
openSdkStyleSqlProjectNoProjectGuidBaseline = await loadBaseline(baselineFolderPath, 'openSdkStyleSqlProjectNoProjectGuidBaseline.xml');
+ openSqlProjectWithAdditionalPublishProfileBaseline = await loadBaseline(baselineFolderPath, 'openSqlProjectWithAdditionalPublishProfileBaseline.xml');
}
async function loadBaseline(baselineFolderPath: string, fileName: string): Promise {
diff --git a/extensions/sql-database-projects/src/test/baselines/openSqlProjectBaseline.xml b/extensions/sql-database-projects/src/test/baselines/openSqlProjectBaseline.xml
index 8e0a321c0d..08548741d2 100644
--- a/extensions/sql-database-projects/src/test/baselines/openSqlProjectBaseline.xml
+++ b/extensions/sql-database-projects/src/test/baselines/openSqlProjectBaseline.xml
@@ -105,6 +105,11 @@
+
+
+
+
+
diff --git a/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithAdditionalPublishProfileBaseline.xml b/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithAdditionalPublishProfileBaseline.xml
new file mode 100644
index 0000000000..3c7e532944
--- /dev/null
+++ b/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithAdditionalPublishProfileBaseline.xml
@@ -0,0 +1,117 @@
+
+
+
+ Debug
+ AnyCPU
+ TestProjectName
+ 2.0
+ 4.1
+ {BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575}
+ Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider
+ Database
+
+
+ TestProjectName
+ TestProjectName
+ 1033, CI
+ BySchemaAndSchemaType
+ True
+ v4.5
+ CS
+ Properties
+ False
+ True
+ True
+
+
+ bin\Release\
+ $(MSBuildProjectName).sql
+ False
+ pdbonly
+ true
+ false
+ true
+ prompt
+ 4
+
+
+ bin\Debug\
+ $(MSBuildProjectName).sql
+ false
+ true
+ full
+ false
+ true
+ true
+ prompt
+ 4
+
+
+ 11.0
+
+ True
+ 11.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MyProdDatabase
+ $(SqlCmdVar__1)
+
+
+ MyBackupDatabase
+ $(SqlCmdVar__2)
+
+
+
+
+ False
+ master
+
+
+ False
+ master
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithAdditionalSqlCmdVariablesBaseline.xml b/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithAdditionalSqlCmdVariablesBaseline.xml
index 173e4f2b62..372a7cb90a 100644
--- a/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithAdditionalSqlCmdVariablesBaseline.xml
+++ b/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithAdditionalSqlCmdVariablesBaseline.xml
@@ -109,6 +109,11 @@
+
+
+
+
+
diff --git a/extensions/sql-database-projects/src/test/project.test.ts b/extensions/sql-database-projects/src/test/project.test.ts
index d9c2ede984..67dbd443aa 100644
--- a/extensions/sql-database-projects/src/test/project.test.ts
+++ b/extensions/sql-database-projects/src/test/project.test.ts
@@ -66,6 +66,12 @@ describe('Project: sqlproj content operations', function (): void {
should(project.postDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'Script.PostDeployment1.sql')).not.equal(undefined, 'File Script.PostDeployment1.sql not read');
should(project.noneDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'Script.PreDeployment2.sql')).not.equal(undefined, 'File Script.PostDeployment2.sql not read');
should(project.noneDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'Tables\\Script.PostDeployment1.sql')).not.equal(undefined, 'File Tables\\Script.PostDeployment1.sql not read');
+
+ // Publish profiles
+ should(project.publishProfiles.length).equal(3);
+ should(project.publishProfiles.find(f => f.type === EntryType.File && f.relativePath === 'TestProjectName_1.publish.xml')).not.equal(undefined, 'Profile TestProjectName_1.publish.xml not read');
+ should(project.publishProfiles.find(f => f.type === EntryType.File && f.relativePath === 'TestProjectName_2.publish.xml')).not.equal(undefined, 'Profile TestProjectName_2.publish.xml not read');
+ should(project.publishProfiles.find(f => f.type === EntryType.File && f.relativePath === 'TestProjectName_3.publish.xml')).not.equal(undefined, 'Profile TestProjectName_3.publish.xml not read');
});
it('Should read Project with Project reference from sqlproj', async function (): Promise {
@@ -1592,6 +1598,30 @@ describe('Project: add SQLCMD Variables', function (): void {
});
});
+describe('Project: add publish profiles', function (): void {
+ before(async function (): Promise {
+ await baselines.loadBaselines();
+ });
+
+ after(async function (): Promise {
+ await testUtils.deleteGeneratedTestFolder();
+ });
+
+ it('Should update .sqlproj with new publish profiles', async function (): Promise {
+ projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline);
+ const project = await Project.openProject(projFilePath);
+ should(Object.keys(project.publishProfiles).length).equal(3);
+
+ // add a new publish profile
+ await project.addPublishProfileToProjFile(path.join(projFilePath, 'TestProjectName_4.publish.xml'));
+
+ should(Object.keys(project.publishProfiles).length).equal(4);
+
+ const projFileText = (await fs.readFile(projFilePath)).toString();
+ should(projFileText).equal(baselines.openSqlProjectWithAdditionalPublishProfileBaseline.trim());
+ });
+});
+
describe('Project: properties', function (): void {
before(async function (): Promise {
await baselines.loadBaselines();