Initial Pre-post deployment changes (#11703)

* Update Open Project to display pre-post deployment scripts in the project tree

* Address comments

* Fixed tests

* Update nonDeployScripts to noneDeployScripts and throw a warning message instead of informational message for prePostDeployCount
This commit is contained in:
Sakshi Sharma
2020-08-19 01:06:29 -07:00
committed by GitHub
parent 0f063d3a2e
commit e90341b3d2
7 changed files with 188 additions and 3 deletions

View File

@@ -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';

View File

@@ -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<string, string> = {};
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

View File

@@ -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;
}

View File

@@ -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<string> {

View File

@@ -94,6 +94,14 @@
<DatabaseVariableLiteralValue>master</DatabaseVariableLiteralValue>
</ArtifactReference>
</ItemGroup>
<ItemGroup>
<PreDeploy Include="Script.PreDeployment1.sql"/>
<None Include="Script.PreDeployment2.sql"/>
</ItemGroup>
<ItemGroup>
<PostDeploy Include="Script.PostDeployment1.sql"/>
<None Include="Tables\Script.PostDeployment1.sql"/>
</ItemGroup>
<Target Name="AfterClean">
<Delete Files="$(BaseIntermediateOutputPath)\project.assets.json" />
</Target>

View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<Name>TestProjectName</Name>
<SchemaVersion>2.0</SchemaVersion>
<ProjectVersion>4.1</ProjectVersion>
<ProjectGuid>{BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575}</ProjectGuid>
<DSP>Microsoft.Data.Tools.Schema.Sql.Sql130DatabaseSchemaProvider</DSP>
<OutputType>Database</OutputType>
<RootPath>
</RootPath>
<RootNamespace>TestProjectName</RootNamespace>
<AssemblyName>TestProjectName</AssemblyName>
<ModelCollation>1033, CI</ModelCollation>
<DefaultFileStructure>BySchemaAndSchemaType</DefaultFileStructure>
<DeployToDatabase>True</DeployToDatabase>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<TargetLanguage>CS</TargetLanguage>
<AppDesignerFolder>Properties</AppDesignerFolder>
<SqlServerVerification>False</SqlServerVerification>
<IncludeCompositeObjects>True</IncludeCompositeObjects>
<TargetDatabaseSet>True</TargetDatabaseSet>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<OutputPath>bin\Release\</OutputPath>
<BuildScriptName>$(MSBuildProjectName).sql</BuildScriptName>
<TreatWarningsAsErrors>False</TreatWarningsAsErrors>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<DefineDebug>false</DefineDebug>
<DefineTrace>true</DefineTrace>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<OutputPath>bin\Debug\</OutputPath>
<BuildScriptName>$(MSBuildProjectName).sql</BuildScriptName>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<DefineDebug>true</DefineDebug>
<DefineTrace>true</DefineTrace>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">11.0</VisualStudioVersion>
<!-- Default to the v11.0 targets path if the targets file for the current VS version is not found -->
<SSDTExists Condition="Exists('$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets')">True</SSDTExists>
<VisualStudioVersion Condition="'$(SSDTExists)' == ''">11.0</VisualStudioVersion>
</PropertyGroup>
<Import Condition="'$(NetCoreBuild)' == 'true'" Project="$(NETCoreTargetsPath)\Microsoft.Data.Tools.Schema.SqlTasks.targets"/>
<Import Condition="'$(NetCoreBuild)' != 'true' AND '$(SQLDBExtensionsRefPath)' != ''" Project="$(SQLDBExtensionsRefPath)\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
<Import Condition="'$(NetCoreBuild)' != 'true' AND '$(SQLDBExtensionsRefPath)' == ''" Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
<ItemGroup>
<PackageReference Condition="'$(NetCoreBuild)' == 'true'" Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties" />
<Folder Include="Tables" />
<Folder Include="Views" />
<Folder Include="Views\Maintenance" />
</ItemGroup>
<ItemGroup>
<Build Include="Tables\Users.sql" />
<Build Include="Tables\Action History.sql" />
<Build Include="Views\Maintenance\Database Performance.sql" />
<Build Include="..\Test\Test.sql" />
</ItemGroup>
<ItemGroup>
<Folder Include="Views\User" />
<Build Include="Views\User\Profile.sql" />
</ItemGroup>
<ItemGroup>
<SqlCmdVariable Include="ProdDatabaseName">
<DefaultValue>MyProdDatabase</DefaultValue>
<Value>$(SqlCmdVar__1)</Value>
</SqlCmdVariable>
<SqlCmdVariable Include="BackupDatabaseName">
<DefaultValue>MyBackupDatabase</DefaultValue>
<Value>$(SqlCmdVar__2)</Value>
</SqlCmdVariable>
</ItemGroup>
<ItemGroup>
<ArtifactReference Condition="'$(NetCoreBuild)' == 'true'" Include="$(NETCoreTargetsPath)\SystemDacpacs\130\master.dacpac">
<SuppressMissingDependenciesErrors>False</SuppressMissingDependenciesErrors>
<DatabaseVariableLiteralValue>master</DatabaseVariableLiteralValue>
</ArtifactReference>
<ArtifactReference Condition="'$(NetCoreBuild)' != 'true'" Include="$(DacPacRootPath)\Extensions\Microsoft\SQLDB\Extensions\SqlServer\130\SqlSchemas\master.dacpac">
<SuppressMissingDependenciesErrors>False</SuppressMissingDependenciesErrors>
<DatabaseVariableLiteralValue>master</DatabaseVariableLiteralValue>
</ArtifactReference>
</ItemGroup>
<ItemGroup>
<PreDeploy Include="Script.PreDeployment1.sql"/>
<PreDeploy Include="Script.PreDeployment2.sql"/>
</ItemGroup>
<ItemGroup>
<PostDeploy Include="Script.PostDeployment1.sql"/>
<None Include="Tables\Script.PostDeployment1.sql"/>
</ItemGroup>
<Target Name="AfterClean">
<Delete Files="$(BaseIntermediateOutputPath)\project.assets.json" />
</Target>
</Project>

View File

@@ -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<void> {
@@ -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<void> {
const stub = sinon.stub(window, 'showWarningMessage').returns(<any>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<void> {
const project = await Project.openProject(projFilePath);