mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-19 01:25:36 -05:00
Load profile support for sql project deploy (#10948)
* load database name from profile.xml * load sqlcmd variables from profile * Add warning text * add tests * fix file filter for windows * add comments * show SQLCMD variables in a table * reset dialog before testing readPublishProfile callback
This commit is contained in:
@@ -41,6 +41,7 @@ export const databaseReferenceSameDatabase = localize('databaseReferenceSameData
|
||||
export const databaseReferenceDifferentDabaseSameServer = localize('databaseReferenceDifferentDabaseSameServer', "Different database, same server");
|
||||
export const databaseReferenceDatabaseName = localize('databaseReferenceDatabaseName', "Database name");
|
||||
export const dacpacFiles = localize('dacpacFiles', "dacpac Files");
|
||||
export const publishSettingsFiles = localize('publishSettingsFiles', "Publish Settings File");
|
||||
export const systemDatabase = localize('systemDatabase', "System Database");
|
||||
export function newObjectNamePrompt(objectType: string) { return localize('newObjectNamePrompt', 'New {0} name:', objectType); }
|
||||
|
||||
@@ -61,6 +62,11 @@ export const connectionRadioButtonLabel = localize('connectionRadioButtonLabel',
|
||||
export const selectConnectionRadioButtonsTitle = localize('selectconnectionRadioButtonsTitle', "Specify connection from:");
|
||||
export const dataSourceDropdownTitle = localize('dataSourceDropdownTitle', "Data source");
|
||||
export const noDataSourcesText = localize('noDataSourcesText', "No data sources in this project");
|
||||
export const loadProfileButtonText = localize('loadProfileButtonText', "Load Profile...");
|
||||
export const profileWarningText = localize('profileWarningText', "⚠Warning: Only database name and SQLCMD variables are able to be loaded from a profile at this time");
|
||||
export const sqlCmdTableLabel = localize('sqlCmdTableLabel', "SQLCMD Variables");
|
||||
export const sqlCmdVariableColumn = localize('sqlCmdVariableColumn', "Variable");
|
||||
export const sqlCmdValueColumn = localize('sqlCmdValueColumn', "Value");
|
||||
|
||||
// Error messages
|
||||
|
||||
@@ -146,6 +152,9 @@ export const NETFrameworkAssembly = 'Microsoft.NETFramework.ReferenceAssemblies'
|
||||
export const VersionNumber = '1.0.0';
|
||||
export const All = 'All';
|
||||
|
||||
// Profile XML names
|
||||
export const targetDatabaseName = 'TargetDatabaseName';
|
||||
|
||||
// SQL connection string components
|
||||
export const initialCatalogSetting = 'Initial Catalog';
|
||||
export const dataSourceSetting = 'Data Source';
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as os from 'os';
|
||||
import * as constants from '../common/constants';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
/**
|
||||
@@ -95,3 +96,20 @@ export function getSafeNonWindowsPath(filePath: string): string {
|
||||
filePath = filePath.split('\\').join('/').split('"').join('');
|
||||
return '"' + filePath + '"';
|
||||
}
|
||||
|
||||
/**
|
||||
* Read SQLCMD variables from xmlDoc and return them
|
||||
* @param xmlDoc xml doc to read SQLCMD variables from. Format must be the same that sqlproj and publish profiles use
|
||||
*/
|
||||
export function readSqlCmdVariables(xmlDoc: any): Record<string, string> {
|
||||
let sqlCmdVariables: Record<string, string> = {};
|
||||
for (let i = 0; i < xmlDoc.documentElement.getElementsByTagName(constants.SqlCmdVariable).length; i++) {
|
||||
const sqlCmdVar = xmlDoc.documentElement.getElementsByTagName(constants.SqlCmdVariable)[i];
|
||||
const varName = sqlCmdVar.getAttribute(constants.Include);
|
||||
|
||||
const varValue = sqlCmdVar.getElementsByTagName(constants.DefaultValue)[0].childNodes[0].nodeValue;
|
||||
sqlCmdVariables[varName] = varValue;
|
||||
}
|
||||
|
||||
return sqlCmdVariables;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as path from 'path';
|
||||
import * as utils from '../common/utils';
|
||||
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
|
||||
import * as templates from '../templates/templates';
|
||||
import * as xmldom from 'xmldom';
|
||||
|
||||
import { Uri, QuickPickItem, WorkspaceFolder, extensions, Extension } from 'vscode';
|
||||
import { IConnectionProfile, TaskExecutionMode } from 'azdata';
|
||||
@@ -19,7 +20,7 @@ import { DeployDatabaseDialog } from '../dialogs/deployDatabaseDialog';
|
||||
import { Project, DatabaseReferenceLocation, SystemDatabase, TargetPlatform } from '../models/project';
|
||||
import { SqlDatabaseProjectTreeViewProvider } from './databaseProjectTreeViewProvider';
|
||||
import { FolderNode } from '../models/tree/fileFolderTreeItem';
|
||||
import { IDeploymentProfile, IGenerateScriptProfile } from '../models/IDeploymentProfile';
|
||||
import { IDeploymentProfile, IGenerateScriptProfile, PublishSettings } from '../models/IDeploymentProfile';
|
||||
import { BaseProjectTreeItem } from '../models/tree/baseTreeItem';
|
||||
import { ProjectRootTreeItem } from '../models/tree/projectTreeItem';
|
||||
import { ImportDataModel } from '../models/api/import';
|
||||
@@ -193,6 +194,7 @@ export class ProjectsController {
|
||||
|
||||
deployDatabaseDialog.deploy = async (proj, prof) => await this.executionCallback(proj, prof);
|
||||
deployDatabaseDialog.generateScript = async (proj, prof) => await this.executionCallback(proj, prof);
|
||||
deployDatabaseDialog.readPublishProfile = async (profileUri) => await this.readPublishProfile(profileUri);
|
||||
|
||||
deployDatabaseDialog.openDialog();
|
||||
|
||||
@@ -216,6 +218,27 @@ export class ProjectsController {
|
||||
}
|
||||
}
|
||||
|
||||
public async readPublishProfile(profileUri: Uri): Promise<PublishSettings> {
|
||||
const profileText = await fs.readFile(profileUri.fsPath);
|
||||
const profileXmlDoc = new xmldom.DOMParser().parseFromString(profileText.toString());
|
||||
|
||||
// read target database name
|
||||
let targetDbName: string = '';
|
||||
let targetDatabaseNameCount = profileXmlDoc.documentElement.getElementsByTagName(constants.targetDatabaseName).length;
|
||||
if (targetDatabaseNameCount > 0) {
|
||||
// if there is more than one TargetDatabaseName nodes, SSDT uses the name in the last one so we'll do the same here
|
||||
targetDbName = profileXmlDoc.documentElement.getElementsByTagName(constants.targetDatabaseName)[targetDatabaseNameCount - 1].textContent;
|
||||
}
|
||||
|
||||
// get all SQLCMD variables to include from the profile
|
||||
let sqlCmdVariables = utils.readSqlCmdVariables(profileXmlDoc);
|
||||
|
||||
return {
|
||||
databaseName: targetDbName,
|
||||
sqlCmdVariables: sqlCmdVariables
|
||||
};
|
||||
}
|
||||
|
||||
public async schemaCompare(treeNode: BaseProjectTreeItem): Promise<void> {
|
||||
// check if schema compare extension is installed
|
||||
if (this.apiWrapper.getExtension(constants.schemaCompareExtensionId)) {
|
||||
|
||||
@@ -28,15 +28,19 @@ export class DeployDatabaseDialog {
|
||||
private targetDatabaseTextBox: azdata.InputBoxComponent | undefined;
|
||||
private connectionsRadioButton: azdata.RadioButtonComponent | undefined;
|
||||
private dataSourcesRadioButton: azdata.RadioButtonComponent | undefined;
|
||||
private loadProfileButton: azdata.ButtonComponent | undefined;
|
||||
private sqlCmdVariablesTable: azdata.TableComponent | undefined;
|
||||
private formBuilder: azdata.FormBuilder | undefined;
|
||||
|
||||
private connection: azdata.connection.Connection | undefined;
|
||||
private connectionIsDataSource: boolean | undefined;
|
||||
private profileSqlCmdVars: Record<string, string> | undefined;
|
||||
|
||||
private toDispose: vscode.Disposable[] = [];
|
||||
|
||||
public deploy: ((proj: Project, profile: IDeploymentProfile) => any) | undefined;
|
||||
public generateScript: ((proj: Project, profile: IGenerateScriptProfile) => any) | undefined;
|
||||
public readPublishProfile: ((profileUri: vscode.Uri) => any) | undefined;
|
||||
|
||||
constructor(private apiWrapper: ApiWrapper, private project: Project) {
|
||||
this.dialog = azdata.window.createModelViewDialog(constants.deployDialogName);
|
||||
@@ -87,6 +91,21 @@ export class DeployDatabaseDialog {
|
||||
this.tryEnableGenerateScriptAndOkButtons();
|
||||
});
|
||||
|
||||
this.loadProfileButton = this.createLoadProfileButton(view);
|
||||
this.sqlCmdVariablesTable = view.modelBuilder.table().withProperties({
|
||||
title: constants.sqlCmdTableLabel,
|
||||
data: this.convertSqlCmdVarsToTableFormat(this.project.sqlCmdVariables),
|
||||
columns: [
|
||||
{
|
||||
value: constants.sqlCmdVariableColumn
|
||||
},
|
||||
{
|
||||
value: constants.sqlCmdValueColumn,
|
||||
}],
|
||||
width: 400,
|
||||
height: 400
|
||||
}).component();
|
||||
|
||||
this.formBuilder = <azdata.FormBuilder>view.modelBuilder.formContainer()
|
||||
.withFormItems([
|
||||
{
|
||||
@@ -100,6 +119,10 @@ export class DeployDatabaseDialog {
|
||||
{
|
||||
title: constants.databaseNameLabel,
|
||||
component: this.targetDatabaseTextBox
|
||||
},
|
||||
{
|
||||
title: constants.profileWarningText,
|
||||
component: <azdata.ButtonComponent>this.loadProfileButton
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -110,6 +133,15 @@ export class DeployDatabaseDialog {
|
||||
width: '100%'
|
||||
});
|
||||
|
||||
// add SQLCMD variables table if the project has any
|
||||
if (Object.keys(this.project.sqlCmdVariables).length > 0) {
|
||||
this.formBuilder.insertFormItem({
|
||||
title: constants.sqlCmdTableLabel,
|
||||
component: <azdata.TableComponent>this.sqlCmdVariablesTable
|
||||
},
|
||||
6);
|
||||
}
|
||||
|
||||
let formModel = this.formBuilder.component();
|
||||
await view.initializeModel(formModel);
|
||||
});
|
||||
@@ -160,11 +192,12 @@ export class DeployDatabaseDialog {
|
||||
}
|
||||
|
||||
public async deployClick(): Promise<void> {
|
||||
const sqlCmdVars = this.getSqlCmdVariablesForDeploy();
|
||||
const profile: IDeploymentProfile = {
|
||||
databaseName: this.getTargetDatabaseName(),
|
||||
upgradeExisting: true,
|
||||
connectionUri: await this.getConnectionUri(),
|
||||
sqlCmdVariables: this.project.sqlCmdVariables
|
||||
sqlCmdVariables: sqlCmdVars
|
||||
};
|
||||
|
||||
this.apiWrapper.closeDialog(this.dialog);
|
||||
@@ -174,10 +207,11 @@ export class DeployDatabaseDialog {
|
||||
}
|
||||
|
||||
public async generateScriptClick(): Promise<void> {
|
||||
const sqlCmdVars = this.getSqlCmdVariablesForDeploy();
|
||||
const profile: IGenerateScriptProfile = {
|
||||
databaseName: this.getTargetDatabaseName(),
|
||||
connectionUri: await this.getConnectionUri(),
|
||||
sqlCmdVariables: this.project.sqlCmdVariables
|
||||
sqlCmdVariables: sqlCmdVars
|
||||
};
|
||||
|
||||
this.apiWrapper.closeDialog(this.dialog);
|
||||
@@ -189,6 +223,18 @@ export class DeployDatabaseDialog {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
private getSqlCmdVariablesForDeploy(): Record<string, string> {
|
||||
// get SQLCMD variables from project
|
||||
let sqlCmdVariables = { ...this.project.sqlCmdVariables };
|
||||
|
||||
// update with SQLCMD variables loaded from profile if there are any
|
||||
for (const key in this.profileSqlCmdVars) {
|
||||
sqlCmdVariables[key] = this.profileSqlCmdVars[key];
|
||||
}
|
||||
|
||||
return sqlCmdVariables;
|
||||
}
|
||||
|
||||
public getTargetDatabaseName(): string {
|
||||
return this.targetDatabaseTextBox?.value ?? '';
|
||||
}
|
||||
@@ -344,6 +390,64 @@ export class DeployDatabaseDialog {
|
||||
return clearButton;
|
||||
}
|
||||
|
||||
private createLoadProfileButton(view: azdata.ModelView): azdata.ButtonComponent {
|
||||
let loadProfileButton: azdata.ButtonComponent = view.modelBuilder.button().withProperties({
|
||||
label: constants.loadProfileButtonText,
|
||||
title: constants.loadProfileButtonText,
|
||||
ariaLabel: constants.loadProfileButtonText,
|
||||
width: '120px'
|
||||
}).component();
|
||||
|
||||
loadProfileButton.onDidClick(async () => {
|
||||
const fileUris = await this.apiWrapper.showOpenDialog(
|
||||
{
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: false,
|
||||
defaultUri: vscode.Uri.parse(this.project.projectFolderPath),
|
||||
filters: {
|
||||
[constants.publishSettingsFiles]: ['publish.xml']
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!fileUris || fileUris.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.readPublishProfile) {
|
||||
const result = await this.readPublishProfile(fileUris[0]);
|
||||
(<azdata.InputBoxComponent>this.targetDatabaseTextBox).value = result.databaseName;
|
||||
this.profileSqlCmdVars = result.sqlCmdVariables;
|
||||
const data = this.convertSqlCmdVarsToTableFormat(this.getSqlCmdVariablesForDeploy());
|
||||
|
||||
(<azdata.TableComponent>this.sqlCmdVariablesTable).updateProperties({
|
||||
data: data
|
||||
});
|
||||
|
||||
// add SQLCMD Variables table if it wasn't there before
|
||||
if (Object.keys(this.project.sqlCmdVariables).length === 0) {
|
||||
this.formBuilder?.insertFormItem({
|
||||
title: constants.sqlCmdTableLabel,
|
||||
component: <azdata.TableComponent>this.sqlCmdVariablesTable
|
||||
},
|
||||
6);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return loadProfileButton;
|
||||
}
|
||||
|
||||
private convertSqlCmdVarsToTableFormat(sqlCmdVars: Record<string, string>): string[][] {
|
||||
let data = [];
|
||||
for (let key in sqlCmdVars) {
|
||||
data.push([key, sqlCmdVars[key]]);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// only enable Generate Script and Ok buttons if all fields are filled
|
||||
private tryEnableGenerateScriptAndOkButtons(): void {
|
||||
if (this.targetConnectionTextBox!.value && this.targetDatabaseTextBox!.value
|
||||
|
||||
@@ -15,3 +15,8 @@ export interface IGenerateScriptProfile {
|
||||
connectionUri: string;
|
||||
sqlCmdVariables?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface PublishSettings {
|
||||
databaseName: string;
|
||||
sqlCmdVariables: Record<string, string>;
|
||||
}
|
||||
|
||||
@@ -64,13 +64,7 @@ export class Project {
|
||||
}
|
||||
|
||||
// find all SQLCMD variables to include
|
||||
for (let i = 0; i < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.SqlCmdVariable).length; i++) {
|
||||
const sqlCmdVar = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.SqlCmdVariable)[i];
|
||||
const varName = sqlCmdVar.getAttribute(constants.Include);
|
||||
|
||||
const varValue = sqlCmdVar.getElementsByTagName(constants.DefaultValue)[0].childNodes[0].nodeValue;
|
||||
this.sqlCmdVariables[varName] = varValue;
|
||||
}
|
||||
this.sqlCmdVariables = utils.readSqlCmdVariables(this.projFileXmlDoc);
|
||||
|
||||
// find all database references to include
|
||||
for (let r = 0; r < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ArtifactReference).length; r++) {
|
||||
|
||||
@@ -19,6 +19,7 @@ export let SSDTUpdatedProjectAfterSystemDbUpdateBaselineWindows: string;
|
||||
export let SSDTUpdatedProjectAfterSystemDbUpdateBaseline: string;
|
||||
export let SSDTProjectBaselineWithCleanTarget: string;
|
||||
export let SSDTProjectBaselineWithCleanTargetAfterUpdate: string;
|
||||
export let publishProfileBaseline: string;
|
||||
|
||||
const baselineFolderPath = __dirname;
|
||||
|
||||
@@ -35,6 +36,7 @@ export async function loadBaselines() {
|
||||
SSDTUpdatedProjectAfterSystemDbUpdateBaseline = await loadBaseline(baselineFolderPath, 'SSDTUpdatedProjectAfterSystemDbUpdateBaseline.xml');
|
||||
SSDTProjectBaselineWithCleanTarget = await loadBaseline(baselineFolderPath, 'SSDTProjectBaselineWithCleanTarget.xml');
|
||||
SSDTProjectBaselineWithCleanTargetAfterUpdate = await loadBaseline(baselineFolderPath, 'SSDTProjectBaselineWithCleanTargetAfterUpdate.xml');
|
||||
publishProfileBaseline = await loadBaseline(baselineFolderPath, 'publishProfileBaseline.publish.xml');
|
||||
}
|
||||
|
||||
async function loadBaseline(baselineFolderPath: string, fileName: string): Promise<string> {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<IncludeCompositeObjects>True</IncludeCompositeObjects>
|
||||
<TargetDatabaseName>targetDb</TargetDatabaseName>
|
||||
<DeployScriptFileName>DatabaseProject1.sql</DeployScriptFileName>
|
||||
<ProfileVersionNumber>1</ProfileVersionNumber>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<SqlCmdVariable Include="ProdDatabaseName">
|
||||
<DefaultValue>MyProdDatabase</DefaultValue>
|
||||
<Value>$(SqlCmdVar__1)</Value>
|
||||
</SqlCmdVariable>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -117,6 +117,7 @@ describe.skip('ProjectsController: project controller operations', function ():
|
||||
|
||||
const deployHoller = 'hello from callback for deploy()';
|
||||
const generateHoller = 'hello from callback for generateScript()';
|
||||
const profileHoller = 'hello from callback for readPublishProfile()';
|
||||
|
||||
let holler = 'nothing';
|
||||
|
||||
@@ -131,6 +132,13 @@ describe.skip('ProjectsController: project controller operations', function ():
|
||||
holler = deployHoller;
|
||||
return undefined;
|
||||
});
|
||||
projController.setup(x => x.readPublishProfile(TypeMoq.It.isAny())).returns(async () => {
|
||||
holler = profileHoller;
|
||||
return {
|
||||
databaseName: '',
|
||||
sqlCmdVariables: {}
|
||||
};
|
||||
});
|
||||
|
||||
projController.setup(x => x.executionCallback(TypeMoq.It.isAny(), TypeMoq.It.is((_): _ is IGenerateScriptProfile => true))).returns(async () => {
|
||||
holler = generateHoller;
|
||||
@@ -146,6 +154,22 @@ describe.skip('ProjectsController: project controller operations', function ():
|
||||
await dialog.generateScriptClick();
|
||||
|
||||
should(holler).equal(generateHoller, 'executionCallback() is supposed to have been setup and called for GenerateScript scenario');
|
||||
|
||||
dialog = await projController.object.deployProject(proj);
|
||||
await projController.object.readPublishProfile(vscode.Uri.parse('test'));
|
||||
|
||||
should(holler).equal(profileHoller, 'executionCallback() is supposed to have been setup and called for ReadPublishProfile scenario');
|
||||
});
|
||||
|
||||
it('Should read database name and SQLCMD variables from publish profile', async function (): Promise<void> {
|
||||
await baselines.loadBaselines();
|
||||
let profilePath = await testUtils.createTestFile(baselines.publishProfileBaseline, 'publishProfile.publish.xml');
|
||||
const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider());
|
||||
|
||||
let result = await projController.readPublishProfile(vscode.Uri.parse(profilePath));
|
||||
should(result.databaseName).equal('targetDb');
|
||||
should(Object.keys(result.sqlCmdVariables).length).equal(1);
|
||||
should(result.sqlCmdVariables['ProdDatabaseName']).equal('MyProdDatabase');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,7 +49,7 @@ export async function generateTestFolderPath(): Promise<string> {
|
||||
return folderPath;
|
||||
}
|
||||
|
||||
async function createTestFile(contents: string, fileName: string, folderPath?: string): Promise<string> {
|
||||
export async function createTestFile(contents: string, fileName: string, folderPath?: string): Promise<string> {
|
||||
folderPath = folderPath ?? await generateTestFolderPath();
|
||||
const filePath = path.join(folderPath, fileName);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user