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 sqlConnectionStringFriendly = localize('sqlConnectionStringFriendly', "SQL connection string");
export const newDatabaseProjectName = localize('newDatabaseProjectName', "New database project name:"); export const newDatabaseProjectName = localize('newDatabaseProjectName', "New database project name:");
export const sqlDatabaseProject = localize('sqlDatabaseProject', "SQL database project"); 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 extractTargetInput = localize('extractTargetInput', "Target for extraction:");
export const selectFileFolder = localize('selectFileFolder', "Select"); export const selectFileFolder = localize('selectFileFolder', "Select");
export function newObjectNamePrompt(objectType: string) { return localize('newObjectNamePrompt', 'New {0} name:', objectType); } 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 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 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 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 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 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); } 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 Build = 'Build';
export const Folder = 'Folder'; export const Folder = 'Folder';
export const Include = 'Include'; 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 // SQL connection string components
export const initialCatalogSetting = 'Initial Catalog'; export const initialCatalogSetting = 'Initial Catalog';

View File

@@ -71,6 +71,9 @@ export class ProjectsController {
await newProject.readProjFile(); await newProject.readProjFile();
this.projects.push(newProject); this.projects.push(newProject);
// Update for round tripping as needed
await newProject.updateProjectForRoundTrip();
// Read datasources.json (if present) // Read datasources.json (if present)
const dataSourcesFilePath = path.join(path.dirname(projectFile.fsPath), constants.dataSourcesFileName); 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 constants from '../common/constants';
import * as utils from '../common/utils'; import * as utils from '../common/utils';
import { Uri } from 'vscode'; import { Uri, window } from 'vscode';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { DataSource } from './dataSources/dataSources'; import { DataSource } from './dataSources/dataSources';
@@ -20,6 +20,7 @@ export class Project {
public projectFileName: string; public projectFileName: string;
public files: ProjectEntry[] = []; public files: ProjectEntry[] = [];
public dataSources: DataSource[] = []; public dataSources: DataSource[] = [];
public importedTargets: string[] = [];
public sqlCmdVariables: Record<string, string> = {}; public sqlCmdVariables: Record<string, string> = {};
public get projectFolderPath() { public get projectFolderPath() {
@@ -41,7 +42,6 @@ export class Project {
this.projFileXmlDoc = new xmldom.DOMParser().parseFromString(projFileText.toString()); this.projFileXmlDoc = new xmldom.DOMParser().parseFromString(projFileText.toString());
// find all folders and files to include // find all folders and files to include
for (let ig = 0; ig < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) { for (let ig = 0; ig < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) {
const itemGroup = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup)[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)); 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 { private findOrCreateItemGroup(containedTag?: string): any {
let outputItemGroup = undefined; let outputItemGroup = undefined;
// find any ItemGroup node that contains files; that's where we'll add // search for a particular item goup if a child type is provided
for (let i = 0; i < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup).length; i++) { if (containedTag) {
const currentItemGroup = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup)[i]; // 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 we find the tag, use the ItemGroup
if (!containedTag || currentItemGroup.getElementsByTagName(containedTag).length > 0) { if (currentItemGroup.getElementsByTagName(containedTag).length > 0) {
outputItemGroup = currentItemGroup; outputItemGroup = currentItemGroup;
break; break;
}
} }
} }
@@ -143,6 +185,34 @@ export class Project {
this.findOrCreateItemGroup(constants.Folder).appendChild(newFolderNode); 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) { private async addToProjFile(entry: ProjectEntry) {
switch (entry.type) { switch (entry.type) {
case EntryType.File: 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> { public async runStreamedCommand(command: string, outputChannel: vscode.OutputChannel, options?: DotNetCommandOptions): Promise<string> {
const stdoutData: string[] = []; const stdoutData: string[] = [];
outputChannel.appendLine(` > ${command}`); outputChannel.appendLine(` > ${command}`);