diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index bc82b9315e..1a36a03dda 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -38,6 +38,7 @@ export const newDatabaseProjectName = localize('newDatabaseProjectName', "New da export const sqlDatabaseProject = localize('sqlDatabaseProject', "SQL database project"); export const yesString = localize('yesString', "Yes"); export const noString = localize('noString', "No"); +export const okString = localize('okString', "Ok"); export const extractTargetInput = localize('extractTargetInput', "Select folder structure for SQL files"); export const extractDatabaseSelection = localize('extractDatabaseSelection', "Select database to import"); export const selectString = localize('selectString', "Select"); @@ -111,6 +112,7 @@ export const databaseSelectionRequired = localize('databaseSelectionRequired', " export const databaseReferenceAlreadyExists = localize('databaseReferenceAlreadyExists', "A reference to this database already exists in this project"); export const ousiderFolderPath = localize('outsideFolderPath', "Items with absolute path outside project folder are not supported. Please make sure the paths in the project file are relative to project folder."); export const parentTreeItemUnknown = localize('parentTreeItemUnknown', "Cannot access parent of provided tree item"); +export const prePostDeployCount = localize('prePostDeployCount', "To successfully build, update the project to have one pre-deployment script and/or one post-deployment script"); 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 noFileExist(fileName: string) { return localize('noFileExist', "File {0} doesn't exist", fileName); } @@ -170,6 +172,9 @@ export const Properties = 'Properties'; export const RelativeOuterPath = '..'; export const ProjectReference = 'ProjectReference'; export const TargetConnectionString = 'TargetConnectionString'; +export const PreDeploy = 'PreDeploy'; +export const PostDeploy = 'PostDeploy'; +export const None = 'None'; // SqlProj File targets export const NetCoreTargets = '$(NETCoreTargetsPath)\\Microsoft.Data.Tools.Schema.SqlTasks.targets'; diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts index 3e18af5d99..33909e279e 100644 --- a/extensions/sql-database-projects/src/models/project.ts +++ b/extensions/sql-database-projects/src/models/project.ts @@ -10,7 +10,7 @@ import * as utils from '../common/utils'; import * as xmlFormat from 'xml-formatter'; import * as os from 'os'; -import { Uri } from 'vscode'; +import { Uri, window } from 'vscode'; import { promises as fs } from 'fs'; import { DataSource } from './dataSources/dataSources'; @@ -25,6 +25,9 @@ export class Project { public importedTargets: string[] = []; public databaseReferences: IDatabaseReferenceProjectEntry[] = []; public sqlCmdVariables: Record = {}; + public preDeployScripts: ProjectEntry[] = []; + public postDeployScripts: ProjectEntry[] = []; + public noneDeployScripts: ProjectEntry[] = []; public get projectFolderPath() { return Uri.file(path.dirname(this.projectFilePath)).fsPath; @@ -70,6 +73,32 @@ export class Project { this.files.push(this.createProjectEntry(folderElements[f].getAttribute(constants.Include), EntryType.Folder)); } } + + // find all pre-deployment scripts to include + let preDeployScriptCount: number = 0; + const preDeploy = itemGroup.getElementsByTagName(constants.PreDeploy); + for (let pre = 0; pre < preDeploy.length; pre++) { + this.preDeployScripts.push(this.createProjectEntry(preDeploy[pre].getAttribute(constants.Include), EntryType.File)); + preDeployScriptCount++; + } + + // find all post-deployment scripts to include + let postDeployScriptCount: number = 0; + const postDeploy = itemGroup.getElementsByTagName(constants.PostDeploy); + for (let post = 0; post < postDeploy.length; post++) { + this.postDeployScripts.push(this.createProjectEntry(postDeploy[post].getAttribute(constants.Include), EntryType.File)); + postDeployScriptCount++; + } + + if (preDeployScriptCount > 1 || postDeployScriptCount > 1) { + window.showWarningMessage(constants.prePostDeployCount, constants.okString); + } + + // find all none-deployment scripts to include + const noneItems = itemGroup.getElementsByTagName(constants.None); + for (let n = 0; n < noneItems.length; n++) { + this.noneDeployScripts.push(this.createProjectEntry(noneItems[n].getAttribute(constants.Include), EntryType.File)); + } } // find all import statements to include diff --git a/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts b/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts index 7f126d7770..4991324a7b 100644 --- a/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts +++ b/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts @@ -55,7 +55,12 @@ export class ProjectRootTreeItem extends BaseProjectTreeItem { * Processes the list of files in a project file to constructs the tree */ private construct() { - for (const entry of this.project.files) { + let treeItemList = this.project.files + .concat(this.project.preDeployScripts) + .concat(this.project.postDeployScripts) + .concat(this.project.noneDeployScripts); + + for (const entry of treeItemList) { if (entry.type !== EntryType.File && entry.relativePath.startsWith(RelativeOuterPath)) { continue; } diff --git a/extensions/sql-database-projects/src/test/baselines/baselines.ts b/extensions/sql-database-projects/src/test/baselines/baselines.ts index e23b700ce8..005e1412c4 100644 --- a/extensions/sql-database-projects/src/test/baselines/baselines.ts +++ b/extensions/sql-database-projects/src/test/baselines/baselines.ts @@ -19,6 +19,7 @@ export let SSDTProjectBaselineWithCleanTargetAfterUpdate: string; export let publishProfileIntegratedSecurityBaseline: string; export let publishProfileSqlLoginBaseline: string; export let openProjectWithProjectReferencesBaseline: string; +export let openSqlProjectWithPrePostDeploymentError: string; const baselineFolderPath = __dirname; @@ -35,6 +36,7 @@ export async function loadBaselines() { publishProfileIntegratedSecurityBaseline = await loadBaseline(baselineFolderPath, 'publishProfileIntegratedSecurityBaseline.publish.xml'); publishProfileSqlLoginBaseline = await loadBaseline(baselineFolderPath, 'publishProfileSqlLoginBaseline.publish.xml'); openProjectWithProjectReferencesBaseline = await loadBaseline(baselineFolderPath, 'openSqlProjectWithProjectReferenceBaseline.xml'); + openSqlProjectWithPrePostDeploymentError = await loadBaseline(baselineFolderPath, 'openSqlProjectWithPrePostDeploymentError.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 ef30358526..5733dab710 100644 --- a/extensions/sql-database-projects/src/test/baselines/openSqlProjectBaseline.xml +++ b/extensions/sql-database-projects/src/test/baselines/openSqlProjectBaseline.xml @@ -94,6 +94,14 @@ master + + + + + + + + diff --git a/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithPrePostDeploymentError.xml b/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithPrePostDeploymentError.xml new file mode 100644 index 0000000000..9ad91fe7ff --- /dev/null +++ b/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithPrePostDeploymentError.xml @@ -0,0 +1,108 @@ + + + + Debug + AnyCPU + TestProjectName + 2.0 + 4.1 + {BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575} + Microsoft.Data.Tools.Schema.Sql.Sql130DatabaseSchemaProvider + 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/project.test.ts b/extensions/sql-database-projects/src/test/project.test.ts index cb86565c5a..792a91aaf1 100644 --- a/extensions/sql-database-projects/src/test/project.test.ts +++ b/extensions/sql-database-projects/src/test/project.test.ts @@ -5,6 +5,7 @@ import * as should from 'should'; import * as path from 'path'; +import * as sinon from 'sinon'; import * as baselines from './baselines/baselines'; import * as testUtils from './testUtils'; import * as constants from '../common/constants'; @@ -12,7 +13,7 @@ import * as constants from '../common/constants'; import { promises as fs } from 'fs'; import { Project, EntryType, TargetPlatform, SystemDatabase, DatabaseReferenceLocation, DacpacReferenceProjectEntry, SqlProjectReferenceProjectEntry } from '../models/project'; import { exists, convertSlashesForSqlProj } from '../common/utils'; -import { Uri } from 'vscode'; +import { Uri, window } from 'vscode'; let projFilePath: string; @@ -46,6 +47,15 @@ describe('Project: sqlproj content operations', function (): void { should(project.databaseReferences.length).equal(1); should(project.databaseReferences[0].databaseName).containEql(constants.master); should(project.databaseReferences[0] instanceof DacpacReferenceProjectEntry).equal(true); + + // Pre-post deployment scripts + should(project.preDeployScripts.length).equal(1); + should(project.postDeployScripts.length).equal(1); + should(project.noneDeployScripts.length).equal(2); + should(project.preDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'Script.PreDeployment1.sql')).not.equal(undefined, 'File Script.PreDeployment1.sql not read'); + 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'); }); it('Should read Project with Project reference from sqlproj', async function (): Promise { @@ -61,6 +71,24 @@ describe('Project: sqlproj content operations', function (): void { should(project.databaseReferences[1] instanceof SqlProjectReferenceProjectEntry).equal(true); }); + it('Should throw warning message while reading Project with more than 1 pre-deploy script from sqlproj', async function (): Promise { + const stub = sinon.stub(window, 'showWarningMessage').returns(Promise.resolve(constants.okString)); + + projFilePath = await testUtils.createTestSqlProjFile(baselines.openSqlProjectWithPrePostDeploymentError); + const project: Project = await Project.openProject(projFilePath); + + should(stub.calledOnce).be.true('showWarningMessage should have been called exactly once'); + should(stub.calledWith(constants.prePostDeployCount)).be.true(`showWarningMessage not called with expected message '${constants.prePostDeployCount}' Actual '${stub.getCall(0).args[0]}'`); + + should(project.preDeployScripts.length).equal(2); + should(project.postDeployScripts.length).equal(1); + should(project.noneDeployScripts.length).equal(1); + should(project.preDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'Script.PreDeployment1.sql')).not.equal(undefined, 'File Script.PreDeployment1.sql not read'); + 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.preDeployScripts.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'); + }); + it('Should add Folder and Build entries to sqlproj', async function (): Promise { const project = await Project.openProject(projFilePath);