Round trip with SSDT (#10563)

* Initial changes

* Round trip feature implementation

* Addressed PR comments

* Addressed comments
This commit is contained in:
Sakshi Sharma
2020-05-27 07:23:47 -07:00
committed by GitHub
parent 7496d09eb9
commit f49bfbc91b
4 changed files with 108 additions and 10 deletions

View File

@@ -23,6 +23,8 @@ export const dataSourcesNodeName = localize('dataSourcesNodeName', "Data Sources
export const sqlConnectionStringFriendly = localize('sqlConnectionStringFriendly', "SQL connection string");
export const newDatabaseProjectName = localize('newDatabaseProjectName', "New database project name:");
export const sqlDatabaseProject = localize('sqlDatabaseProject', "SQL database project");
export const yesString = localize('yesString', "Yes");
export const noString = localize('noString', "No");
export const extractTargetInput = localize('extractTargetInput', "Target for extraction:");
export const selectFileFolder = localize('selectFileFolder', "Select");
export function newObjectNamePrompt(objectType: string) { return localize('newObjectNamePrompt', 'New {0} name:', objectType); }
@@ -60,6 +62,7 @@ export const projectLocationNotEmpty = localize('projectLocationNotEmpty', "Curr
export const extractTargetRequired = localize('extractTargetRequired', "Target information for extract is required to import database to project.");
export const schemaCompareNotInstalled = localize('schemaCompareNotInstalled', "Schema compare extension installation is required to run schema compare");
export const buildDacpacNotFound = localize('buildDacpacNotFound', "Dacpac created from build not found");
export const updateProjectForRoundTrip = localize('updateProjectForRoundTrip', "To build this project, Azure Data Studio needs to update targets and references. If the project is created in SSDT, it will continue to work in both tools. Do you want Azure Data Studio to update the project?");
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); }
@@ -82,6 +85,28 @@ export const ItemGroup = 'ItemGroup';
export const Build = 'Build';
export const Folder = 'Folder';
export const Include = 'Include';
export const Import = 'Import';
export const Project = 'Project';
export const Condition = 'Condition';
export const PackageReference = 'PackageReference';
export const Version = 'Version';
export const PrivateAssets = 'PrivateAssets';
// SqlProj File targets
export const NetCoreTargets = '$(NETCoreTargetsPath)\\Microsoft.Data.Tools.Schema.SqlTasks.targets';
export const SqlDbTargets = '$(SQLDBExtensionsRefPath)\\Microsoft.Data.Tools.Schema.SqlTasks.targets';
export const MsBuildtargets = '$(MSBuildExtensionsPath)\\Microsoft\\VisualStudio\\v$(VisualStudioVersion)\\SSDT\\Microsoft.Data.Tools.Schema.SqlTasks.targets';
export const NetCoreCondition = '\'$(NetCoreBuild)\' == \'true\'';
export const SqlDbPresentCondition = '\'$(SQLDBExtensionsRefPath)\' != \'\'';
export const SqlDbNotPresentCondition = '\'$(SQLDBExtensionsRefPath)\' == \'\'';
export const RoundTripSqlDbPresentCondition = '\'$(NetCoreBuild)\' != \'true\' AND \'$(SQLDBExtensionsRefPath)\' != \'\'';
export const RoundTripSqlDbNotPresentCondition = '\'$(NetCoreBuild)\' != \'true\' AND \'$(SQLDBExtensionsRefPath)\' == \'\'';
// SqlProj Reference Assembly Information
export const NETFrameworkAssembly = 'Microsoft.NETFramework.ReferenceAssemblies';
export const VersionNumber = '1.0.0';
export const All = 'All';
// SQL connection string components
export const initialCatalogSetting = 'Initial Catalog';

View File

@@ -71,6 +71,9 @@ export class ProjectsController {
await newProject.readProjFile();
this.projects.push(newProject);
// Update for round tripping as needed
await newProject.updateProjectForRoundTrip();
// Read datasources.json (if present)
const dataSourcesFilePath = path.join(path.dirname(projectFile.fsPath), constants.dataSourcesFileName);

View File

@@ -8,7 +8,7 @@ import * as xmldom from 'xmldom';
import * as constants from '../common/constants';
import * as utils from '../common/utils';
import { Uri } from 'vscode';
import { Uri, window } from 'vscode';
import { promises as fs } from 'fs';
import { DataSource } from './dataSources/dataSources';
@@ -20,6 +20,7 @@ export class Project {
public projectFileName: string;
public files: ProjectEntry[] = [];
public dataSources: DataSource[] = [];
public importedTargets: string[] = [];
public sqlCmdVariables: Record<string, string> = {};
public get projectFolderPath() {
@@ -41,7 +42,6 @@ export class Project {
this.projFileXmlDoc = new xmldom.DOMParser().parseFromString(projFileText.toString());
// find all folders and files to include
for (let ig = 0; ig < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) {
const itemGroup = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup)[ig];
@@ -53,6 +53,45 @@ export class Project {
this.files.push(this.createProjectEntry(itemGroup.getElementsByTagName(constants.Folder)[f].getAttribute(constants.Include), EntryType.Folder));
}
}
// find all import statements to include
for (let i = 0; i < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.Import).length; i++) {
const importTarget = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.Import)[i];
this.importedTargets.push(importTarget.getAttribute(constants.Project));
}
}
public async updateProjectForRoundTrip() {
if (this.importedTargets.includes(constants.NetCoreTargets)) {
return;
}
window.showWarningMessage(constants.updateProjectForRoundTrip, constants.yesString, constants.noString).then(async (result) => {
if (result === constants.yesString) {
await fs.copyFile(this.projectFilePath, this.projectFilePath + '_backup');
await this.updateImportToSupportRoundTrip();
await this.updatePackageReferenceInProjFile();
}
});
}
private async updateImportToSupportRoundTrip(): Promise<void> {
// update an SSDT project to include Net core target information
for (let i = 0; i < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.Import).length; i++) {
const importTarget = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.Import)[i];
let condition = importTarget.getAttribute(constants.Condition);
let project = importTarget.getAttribute(constants.Project);
if (condition === constants.SqlDbPresentCondition && project === constants.SqlDbTargets) {
await this.updateImportedTargetsToProjFile(constants.RoundTripSqlDbPresentCondition, project, importTarget);
}
if (condition === constants.SqlDbNotPresentCondition && project === constants.MsBuildtargets) {
await this.updateImportedTargetsToProjFile(constants.RoundTripSqlDbNotPresentCondition, project, importTarget);
}
}
await this.updateImportedTargetsToProjFile(constants.NetCoreCondition, constants.NetCoreTargets, undefined);
}
/**
@@ -109,14 +148,17 @@ export class Project {
private findOrCreateItemGroup(containedTag?: string): any {
let outputItemGroup = undefined;
// find any ItemGroup node that contains files; that's where we'll add
for (let i = 0; i < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup).length; i++) {
const currentItemGroup = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup)[i];
// search for a particular item goup if a child type is provided
if (containedTag) {
// find any ItemGroup node that contains files; that's where we'll add
for (let ig = 0; ig < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) {
const currentItemGroup = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup)[ig];
// if we're not hunting for a particular child type, or if we are and we find it, use the ItemGroup
if (!containedTag || currentItemGroup.getElementsByTagName(containedTag).length > 0) {
outputItemGroup = currentItemGroup;
break;
// if we find the tag, use the ItemGroup
if (currentItemGroup.getElementsByTagName(containedTag).length > 0) {
outputItemGroup = currentItemGroup;
break;
}
}
}
@@ -143,6 +185,34 @@ export class Project {
this.findOrCreateItemGroup(constants.Folder).appendChild(newFolderNode);
}
private async updateImportedTargetsToProjFile(condition: string, project: string, oldImportNode?: any): Promise<any> {
const importNode = this.projFileXmlDoc.createElement(constants.Import);
importNode.setAttribute(constants.Condition, condition);
importNode.setAttribute(constants.Project, project);
if (oldImportNode) {
this.projFileXmlDoc.documentElement.replaceChild(importNode, oldImportNode);
}
else {
this.projFileXmlDoc.documentElement.appendChild(importNode, oldImportNode);
}
await this.serializeToProjFile(this.projFileXmlDoc);
return importNode;
}
private async updatePackageReferenceInProjFile(): Promise<void> {
const packageRefNode = this.projFileXmlDoc.createElement(constants.PackageReference);
packageRefNode.setAttribute(constants.Condition, constants.NetCoreCondition);
packageRefNode.setAttribute(constants.Include, constants.NETFrameworkAssembly);
packageRefNode.setAttribute(constants.Version, constants.VersionNumber);
packageRefNode.setAttribute(constants.PrivateAssets, constants.All);
this.findOrCreateItemGroup(constants.PackageReference).appendChild(packageRefNode);
await this.serializeToProjFile(this.projFileXmlDoc);
}
private async addToProjFile(entry: ProjectEntry) {
switch (entry.type) {
case EntryType.File:

View File

@@ -112,7 +112,7 @@ export class NetCoreTool {
}
}
// spawns the dotnet command with aruments and redirects the error and output to ADS output channel
// spawns the dotnet command with arguments and redirects the error and output to ADS output channel
public async runStreamedCommand(command: string, outputChannel: vscode.OutputChannel, options?: DotNetCommandOptions): Promise<string> {
const stdoutData: string[] = [];
outputChannel.appendLine(` > ${command}`);