diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts index 4edb1ec151..a7382526e6 100644 --- a/extensions/sql-database-projects/src/models/project.ts +++ b/extensions/sql-database-projects/src/models/project.ts @@ -863,7 +863,7 @@ export class Project implements ISqlProject { return outputItemGroup; } - private addFileToProjFile(path: string, xmlTag: string, attributes?: Map): void { + private async addFileToProjFile(path: string, xmlTag: string, attributes?: Map): Promise { let itemGroup; if (xmlTag === constants.PreDeploy || xmlTag === constants.PostDeploy) { @@ -876,6 +876,14 @@ export class Project implements ISqlProject { } } else { + const currentFiles = await this.readFilesInProject(); + + // don't need to add an entry if it's already included by a glob pattern + // unless it has an attribute that needs to be added, like external streaming job which needs it so it can be determined if validation can run on it + if (attributes?.size === 0 && currentFiles.find(f => f.relativePath === utils.convertSlashesForSqlProj(path))) { + return; + } + itemGroup = this.findOrCreateItemGroup(xmlTag); } @@ -1202,7 +1210,7 @@ export class Project implements ISqlProject { private async addToProjFile(entry: ProjectEntry, xmlTag?: string, attributes?: Map): Promise { switch (entry.type) { case EntryType.File: - this.addFileToProjFile((entry).relativePath, xmlTag ? xmlTag : constants.Build, attributes); + await this.addFileToProjFile((entry).relativePath, xmlTag ? xmlTag : constants.Build, attributes); break; case EntryType.Folder: this.addFolderToProjFile((entry).relativePath); @@ -1351,6 +1359,11 @@ export class Project implements ISqlProject { // If folder doesn't exist, create it await fs.mkdir(absoluteFolderPath, { recursive: true }); + // don't need to add the folder to the sqlproj if this is an msbuild sdk style project because globbing will get the folders + if (this.isMsbuildSdkStyleProject) { + return this.createFileProjectEntry(relativeFolderPath, EntryType.Folder); + } + // Add project file entries for all folders in the path. // SSDT expects all folders to be explicitly listed in the project file, so we construct // folder paths for all intermediate folders and ensure they are present in the project as well. diff --git a/extensions/sql-database-projects/src/test/project.test.ts b/extensions/sql-database-projects/src/test/project.test.ts index 2ba63fd292..69dd60d596 100644 --- a/extensions/sql-database-projects/src/test/project.test.ts +++ b/extensions/sql-database-projects/src/test/project.test.ts @@ -947,6 +947,46 @@ describe('Project: Msbuild sdk style project content operations', function (): v // should(project.files.filter(f => f.relativePath === 'file1.sql').length).equal(0); }); + + it('Should only add Build entries to sqlproj for files not included by project folder glob and external streaming jobs', async function (): Promise { + projFilePath = await testUtils.createTestSqlProjFile(baselines.openNewStyleSqlProjectBaseline); + const project = await Project.openProject(projFilePath); + + const folderPath = 'Stored Procedures'; + const scriptPath = path.join(folderPath, 'Fake Stored Proc.sql'); + const scriptContents = 'SELECT \'This is not actually a stored procedure.\''; + + const scriptPathTagged = 'Fake External Streaming Job.sql'; + const scriptContentsTagged = 'EXEC sys.sp_create_streaming_job \'job\', \'SELECT 7\''; + + const outsideFolderScriptPath = path.join('..', 'Other Fake Stored Proc.sql'); + const outsideFolderScriptContents = 'SELECT \'This is also not actually a stored procedure.\''; + + const otherFolderPath = 'OtherFolder'; + + await project.addScriptItem(scriptPath, scriptContents); + await project.addScriptItem(scriptPathTagged, scriptContentsTagged, templates.externalStreamingJob); + await project.addScriptItem(outsideFolderScriptPath, outsideFolderScriptContents); + await project.addFolderItem(otherFolderPath); + + const newProject = await Project.openProject(projFilePath); + + should(newProject.files.find(f => f.type === EntryType.Folder && f.relativePath === convertSlashesForSqlProj(folderPath))).not.equal(undefined); + should(newProject.files.find(f => f.type === EntryType.File && f.relativePath === convertSlashesForSqlProj(scriptPath))).not.equal(undefined); + should(newProject.files.find(f => f.type === EntryType.File && f.relativePath === convertSlashesForSqlProj(scriptPathTagged))).not.equal(undefined); + should(newProject.files.find(f => f.type === EntryType.File && f.relativePath === convertSlashesForSqlProj(scriptPathTagged))?.sqlObjectType).equal(constants.ExternalStreamingJob); + should(newProject.files.find(f => f.type === EntryType.File && f.relativePath === convertSlashesForSqlProj(outsideFolderScriptPath))).not.equal(undefined); + should(newProject.files.find(f => f.type === EntryType.Folder && f.relativePath === convertSlashesForSqlProj(otherFolderPath))).not.equal(undefined); + + // only the external streaming job and file outside of the project folder should have been added to the sqlproj + const projFileText = (await fs.readFile(projFilePath)).toString(); + should(projFileText.includes('')).equal(false, projFileText); + should(projFileText.includes('')).equal(false, projFileText); + should(projFileText.includes('')).equal(true, projFileText); + should(projFileText.includes('')).equal(true, projFileText); + should(projFileText.includes('')).equal(false, projFileText); + }); + }); describe('Project: add SQLCMD Variables', function (): void {